@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/README.md +38 -39
- package/dist/daemon_main.js +25 -42
- package/dist/daemon_main.js.map +1 -1
- package/dist/engine-Xe7nH-1i.js +267 -0
- package/dist/engine-Xe7nH-1i.js.map +1 -0
- package/dist/index.d.ts +181 -48
- package/dist/index.js +60 -56
- package/dist/index.js.map +1 -1
- package/package.json +7 -11
- package/src/index.ts +59 -81
- package/src/lib/binding.ts +89 -0
- package/src/lib/client.ts +8 -12
- package/src/lib/config.ts +11 -66
- package/src/lib/daemon.ts +70 -58
- package/src/lib/endpoint.ts +19 -12
- package/src/lib/engine.ts +168 -0
- package/src/lib/platform.ts +1 -7
- package/src/lib/request.ts +4 -15
- package/src/types.ts +19 -45
- package/dist/native.d.ts +0 -66
- package/dist/native.js +0 -127
- package/dist/native.js.map +0 -1
- package/dist/platform-DU8DYqmA.js +0 -32
- package/dist/platform-DU8DYqmA.js.map +0 -1
- package/dist/pool-BSgS6vkr.js +0 -356
- package/dist/pool-BSgS6vkr.js.map +0 -1
- package/dist/request-qZXS3N9f.js +0 -43
- package/dist/request-qZXS3N9f.js.map +0 -1
- package/dist/types-x9HtkzeE.d.ts +0 -156
- package/src/lib/pool.ts +0 -243
- package/src/lib/worker.ts +0 -220
- package/src/native.ts +0 -234
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-Xe7nH-1i.js","names":["require","platformPackage.packageDir","platformPackage.packageName","binding.load","binding.directory"],"sources":["../src/lib/config.ts","../src/lib/endpoint.ts","../src/lib/protocol.ts","../src/lib/request.ts","../src/lib/platform.ts","../src/lib/binding.ts","../src/lib/engine.ts"],"sourcesContent":["import type {StartOptions} from '../types.js';\n\n// StartOptions with every hole filled in. `cacheDir` is still nullable here\n// because null is an answer -- \"no disk cache\" -- and not an absent one.\nexport interface ResolvedStartOptions {\n cacheDir: string|null;\n userAgent?: string;\n resourceDir?: string;\n}\n\n// The one place that decides what \"no options\" means.\n//\n// It is shared rather than duplicated because the daemon's address is a hash of\n// its configuration: if two callers filled in defaults even slightly\n// differently, one would compute an address no daemon is listening on and\n// start a second engine next to the first one that was already warm. See\n// endpoint.ts.\n//\n// The default for `cacheDir` is null -- no disk cache. A program holding the\n// engine is often short-lived, and a cache it never reads twice is a directory\n// it leaves behind. The daemon, which is the case where a cache does pay for\n// itself, is also the case where the caller is already passing options.\nfunction resolveStartOptions(options: StartOptions = {}): ResolvedStartOptions {\n return {\n cacheDir: options.cacheDir ?? null,\n userAgent: options.userAgent,\n resourceDir: options.resourceDir,\n };\n}\n\nexport {resolveStartOptions};\n","import crypto from 'node:crypto';\nimport os from 'node:os';\nimport path from 'node:path';\n\n// What endpointFor() needs to know: a resolved configuration, plus the two\n// ways of overriding the address it would derive from one.\nexport interface EndpointOptions {\n cacheDir?: string|null;\n userAgent?: string;\n resourceDir?: string;\n name?: string;\n endpoint?: string;\n}\n\n// Where a daemon listens, derived from what it was asked to be.\n//\n// The address is a hash of the configuration -- cache root, user agent,\n// resource directory -- rather than a fixed name, because attaching to\n// whatever daemon happens to be up would mean rendering with someone else's\n// settings. Two configurations are two daemons; the same configuration, from\n// any process, is one.\n//\n// Every field of EndpointOptions is optional, so nothing here fails to compile\n// when a field is dropped from the configuration -- it just stops being part\n// of the identity, and every caller collapses onto one address. That happened\n// once, when the worker pool went away and this was left hashing three fields\n// that no longer existed. If a field is added to StartOptions and it changes\n// what the engine renders, it belongs in the array below.\n//\n// A caller who wants a daemon by name instead of by configuration passes\n// `name`, which replaces the hash. That is the escape hatch for a service that\n// starts its daemon deliberately and wants clients to find it without\n// repeating the configuration.\nfunction endpointKey(options: EndpointOptions): string {\n if (options.name) {\n return String(options.name);\n }\n const identity = JSON.stringify([\n options.cacheDir === null || options.cacheDir === undefined ?\n null :\n path.resolve(options.cacheDir),\n options.userAgent ?? null,\n options.resourceDir ? path.resolve(options.resourceDir) : null,\n ]);\n return crypto.createHash('sha256').update(identity).digest('hex').slice(0, 16);\n}\n\n// Windows has named pipes and no filesystem sockets; POSIX has the reverse.\n// Both are net.connect() addresses, which is the only reason the rest of the\n// daemon can ignore the difference.\n//\n// The pipe namespace is per-machine but the socket path is per-user, so the\n// uid goes in the POSIX name to keep two users on one host from colliding on a\n// path only one of them can open.\nfunction endpointFor(options: EndpointOptions = {}): string {\n if (options.endpoint) {\n return options.endpoint;\n }\n if (process.env.SHOTIUM_ENDPOINT) {\n return process.env.SHOTIUM_ENDPOINT;\n }\n const key = endpointKey(options);\n if (process.platform === 'win32') {\n return `\\\\\\\\.\\\\pipe\\\\shotium-${key}`;\n }\n const uid = typeof process.getuid === 'function' ? process.getuid() : 0;\n return path.join(os.tmpdir(), `shotium-${uid}-${key}.sock`);\n}\n\nexport {endpointFor, endpointKey};\n","// The wire format shotium.exe --serve speaks, in both directions: a 4-byte\n// little-endian length followed by that many bytes.\n//\n// Length-prefixed rather than line-delimited because the payload is binary and\n// a newline inside a PNG is not a message boundary. See shot/shot_server.h for\n// the same description from the other end.\n//\n// -> [len][{\"file\":\"...\",\"width\":1248,...}]\n// <- [len][{\"ok\":true,\"bytes\":97756}] [len][<PNG bytes>]\n// <- [len][{\"ok\":false,\"error\":\"...\"}] [0]\n\nconst HEADER_BYTES = 4;\n\nfunction encodeFrame(payload: Buffer): Buffer {\n const header = Buffer.allocUnsafe(HEADER_BYTES);\n header.writeUInt32LE(payload.length, 0);\n return Buffer.concat([header, payload]);\n}\n\nfunction encodeRequest(request: unknown): Buffer {\n return encodeFrame(Buffer.from(JSON.stringify(request), 'utf8'));\n}\n\n// Reassembles frames out of whatever sizes the pipe hands over.\n//\n// A stream is not a sequence of messages: one read can carry half a header, or\n// three responses and the start of a fourth. Everything downstream assumes\n// whole frames, so this is the only place that has to know that.\nclass FrameReader {\n private buffer: Buffer = Buffer.alloc(0);\n\n push(chunk: Buffer): void {\n this.buffer = this.buffer.length === 0 ?\n chunk :\n Buffer.concat([this.buffer, chunk]);\n }\n\n // The next complete frame, or null when there is not one yet.\n next(): Buffer|null {\n if (this.buffer.length < HEADER_BYTES) {\n return null;\n }\n const length = this.buffer.readUInt32LE(0);\n if (this.buffer.length < HEADER_BYTES + length) {\n return null;\n }\n const frame = this.buffer.subarray(HEADER_BYTES, HEADER_BYTES + length);\n this.buffer = this.buffer.subarray(HEADER_BYTES + length);\n return frame;\n }\n}\n\nexport {HEADER_BYTES, encodeFrame, encodeRequest, FrameReader};\n","import type {Clip, PageGotoParams, ScreenshotOptions} from '../types.js';\n\nconst DEFAULT_TIMEOUT_MS = 30000;\n\n// What actually goes down the pipe: ScreenshotOptions with the viewport\n// flattened -- see toRequest below for why.\nexport interface WireRequest {\n file: string;\n type?: 'png'|'jpeg'|'webp';\n fullPage?: boolean;\n selector?: string;\n quality?: number;\n scale?: number;\n omitBackground?: boolean;\n path?: string;\n pageGotoParams?: PageGotoParams;\n clip?: Clip;\n allowFileAccess?: boolean;\n width?: number;\n height?: number;\n}\n\n// Everything the worker understands, and nothing else. An unknown field is a\n// typo, and a typo that is silently dropped is a screenshot that quietly\n// ignored what was asked for -- so this rejects rather than filters.\n//\n// It is a runtime check even though the argument has a type, because the\n// argument having a type says nothing about a caller who is not compiled\n// against it: a JavaScript program, or a JSON body from somewhere else.\nconst WIRE_FIELDS = new Set([\n 'file',\n 'type',\n 'fullPage',\n 'selector',\n 'quality',\n 'scale',\n 'omitBackground',\n 'path',\n 'pageGotoParams',\n 'clip',\n 'viewport',\n 'allowFileAccess',\n]);\n\n// One ScreenshotOptions, checked and flattened into what goes on the wire.\n//\n// It lives here rather than in index.ts because the engine in this process and\n// the daemon both send it: a request that is valid through one entry point and\n// rejected through the other would be a difference nobody asked for.\nfunction toRequest(options: ScreenshotOptions): WireRequest {\n if (!options || typeof options !== 'object') {\n throw new TypeError('shotium: screenshot(options) needs an object');\n }\n if (typeof options.file !== 'string' || options.file.length === 0) {\n throw new TypeError('shotium: options.file is required');\n }\n\n const request: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(options)) {\n if (value === undefined) {\n continue;\n }\n if (!WIRE_FIELDS.has(key)) {\n throw new TypeError(`shotium: unknown option \"${key}\"`);\n }\n request[key] = value;\n }\n\n // The viewport is flattened because the worker takes width and height at the\n // top level: it is one screenshot's frame, not a nested object on the wire.\n if (request.viewport) {\n const {width, height} = request.viewport as {\n width?: number,\n height?: number,\n };\n delete request.viewport;\n if (width !== undefined) {\n request.width = width;\n }\n if (height !== undefined) {\n request.height = height;\n }\n }\n return request as unknown as WireRequest;\n}\n\nfunction timeoutFor(options: ScreenshotOptions): number {\n const timeout = options.pageGotoParams && options.pageGotoParams.timeout;\n return typeof timeout === 'number' ? timeout : DEFAULT_TIMEOUT_MS;\n}\n\nexport {\n DEFAULT_TIMEOUT_MS,\n WIRE_FIELDS,\n timeoutFor,\n toRequest,\n};\n","import {createRequire} from 'node:module';\nimport path from 'node:path';\n\n// require.resolve is the resolver, and ESM has no synchronous equivalent\n// that answers for a package that may not be installed at all.\nconst require = createRequire(import.meta.url);\n\n// Which package carries the engine for this machine.\n//\n// The engine is not in this package and cannot be: it is a Chromium build,\n// 41 MB per platform and architecture, six of them, and `npm install` is never\n// going to produce one. So the bytes live in six packages of their own and\n// this one depends on all six as optionalDependencies with `os` and `cpu` set,\n// which is npm's way of saying \"install the one that matches this machine and\n// skip the other five\". A machine nobody builds for installs none of them and\n// still gets a working package -- it just has to be pointed at an engine.\n//\n// The alternative, a postinstall script that downloads a tarball, was not\n// chosen. It defeats a lockfile, which is supposed to pin what you get; it\n// fails behind a registry mirror, which is the one place a large dependency\n// most needs to work; and it runs code at install time in exchange for saving\n// nothing that npm was not already doing.\n//\n// Key and name are both `${process.platform}-${process.arch}`, so the table is\n// the identity map with a prefix on it. That is deliberate: the value npm\n// matches `os` and `cpu` against is process.platform, and a package named for\n// anything else makes the reader hold two spellings of one machine in their\n// head. It is also what every other package of this shape does -- esbuild,\n// swc, lightningcss all publish darwin-arm64 and win32-x64.\n//\n// The release archives spell it win/mac instead -- shotium-mac-arm64.7z --\n// and that is not going to change either. They are downloaded by people, and\n// `mac` is what people call it. So the two spellings do differ, in the one\n// place where each is right: the registry gets node's, the download page gets\n// the reader's.\nconst PACKAGES: Readonly<Record<string, string>> = {\n 'win32-x64': '@shotkit/shotium-win32-x64',\n 'win32-arm64': '@shotkit/shotium-win32-arm64',\n 'darwin-x64': '@shotkit/shotium-darwin-x64',\n 'darwin-arm64': '@shotkit/shotium-darwin-arm64',\n 'linux-x64': '@shotkit/shotium-linux-x64',\n 'linux-arm64': '@shotkit/shotium-linux-arm64',\n};\n\nfunction packageName(\n platform: string = process.platform,\n arch: string = process.arch): string|null {\n return PACKAGES[`${platform}-${arch}`] ?? null;\n}\n\n// Where the matching platform package unpacked, or null if it is not installed.\n//\n// require.resolve rather than a path built from the module's own location: the\n// package can be hoisted to a workspace root, nested under this one, or left\n// in a pnpm store with a symlink pointing at it, and the resolver is the only\n// thing that knows which of those happened.\nfunction packageDir(): string|null {\n const name = packageName();\n if (!name) {\n return null;\n }\n try {\n return path.dirname(require.resolve(`${name}/package.json`));\n } catch {\n return null;\n }\n}\n\nexport {PACKAGES, packageDir, packageName};\n","import fs from 'node:fs';\nimport {createRequire} from 'node:module';\nimport path from 'node:path';\nimport {fileURLToPath} from 'node:url';\n\nimport * as platformPackage from './platform.js';\n\n// A .node addon is a CommonJS artefact: there is no ESM loader for one.\nconst require = createRequire(import.meta.url);\n\n// ESM has no __dirname. This is the same thing, from the module's own URL.\nconst HERE = path.dirname(fileURLToPath(import.meta.url));\n\n/**\n * The engine handle the addon hands back. Opaque on purpose: everything that\n * can be done with it is a call on the binding below.\n */\nexport type Engine = unknown;\n\n/** What native/binding.cc exports. See shot/shot_api.h for the C ABI. */\nexport interface NativeBinding {\n create(optionsJson: string): Engine;\n destroy(engine: Engine): void;\n purge(engine: Engine, releaseWorkingSet: boolean): void;\n capture(engine: Engine, requestJson: string): Promise<Buffer>;\n}\n\n// Where the addon and the library beside it live.\n//\n// The platform package is what ships -- the .node sits next to the shared\n// library it is linked against, which is the whole reason the two travel in\n// one package rather than two. native/build/Release is where node-gyp puts a\n// local build; it exists in a checkout and not in an install, so the two never\n// compete in practice. Both paths are relative to this file's build output,\n// which is one directory below the package root.\nfunction candidates(): string[] {\n const found: string[] = [];\n const dir = platformPackage.packageDir();\n if (dir) {\n found.push(path.join(dir, 'shotium.node'));\n }\n found.push(\n path.join(HERE, '..', 'native', 'build', 'Release', 'shotium.node'));\n return found;\n}\n\nlet binding: NativeBinding|null = null;\nlet loadedFrom: string|null = null;\n\n/**\n * The addon, loaded once. Throws if there is none for this platform, which is\n * the only failure this package cannot work around: there is nothing else to\n * fall back to.\n */\nexport function load(): NativeBinding {\n if (binding) {\n return binding;\n }\n const tried = candidates();\n for (const candidate of tried) {\n if (!fs.existsSync(candidate)) {\n continue;\n }\n // Not wrapped in a try: a .node that is there and will not load is a\n // broken installation, and the loader's own message -- a missing\n // dependency, an architecture mismatch -- says more than anything that\n // could be substituted for it.\n binding = require(candidate) as NativeBinding;\n loadedFrom = path.dirname(candidate);\n return binding;\n }\n const expected = platformPackage.packageName();\n throw new Error(\n 'shotium: no engine for this platform.\\n' +\n ` looked in:\\n ${tried.join('\\n ')}\\n` +\n (expected ?\n ` It ships in ${expected}, which npm installs as an optional ` +\n 'dependency of this package. If the install skipped optional ' +\n 'dependencies, it is not there.\\n' :\n ` There is no build for ${process.platform}-${process.arch}.\\n`));\n}\n\n/**\n * The directory the addon came from, or null before the first load(). The\n * resource packs ship beside it, which is what this is for.\n */\nexport function directory(): string|null {\n return loadedFrom;\n}\n","import * as binding from './binding.js';\nimport type {Engine as Handle} from './binding.js';\nimport {toRequest} from './request.js';\nimport type {WireRequest} from './request.js';\nimport type {PurgeOptions, ScreenshotOptions, StartOptions} from '../types.js';\n\nimport {resolveStartOptions} from './config.js';\n\n// One per process, ever. Not one at a time -- one.\n//\n// This is not a rule of this file, it is what Blink is: initialising it writes\n// process-wide statics it has no path to undo, so shot_engine_destroy() gives\n// back what it can and the process still cannot make another. The C API\n// returns SHOT_ERR_STATE for a second create whether or not the first is\n// still alive. See shot/shot_api.h.\n//\n// So `stop()` is final for the process, and this flag exists to say that in\n// words at the call site. Without it a caller who stops and starts again gets\n// SHOT_ERR_STATE out of the addon -- a true error, arriving one layer too deep\n// to explain that the answer is a second process rather than a retry.\nlet startedInThisProcess = false;\n\n/**\n * Blink, in this process, and the queue in front of it.\n *\n * There is one renderer and there is no way to have two. Blink is a\n * process-wide singleton: it is initialised once, there is no path to a second\n * one, and `worker_threads` do not change that because they share the process.\n * So captures are serialised however many callers there are, and a program\n * that wants four at once wants four processes.\n *\n * The queue is not about fairness. Each capture occupies a libuv thread pool\n * thread for as long as the render takes, and there are four of those by\n * default, shared with fs and dns -- so letting four screenshots go at once\n * would stall the host's file reads for a fifth of a second at a time while\n * gaining nothing, since the engine serialises them anyway.\n */\nexport class Engine {\n private handle: Handle|null = null;\n private stopped = false;\n private tail: Promise<unknown> = Promise.resolve();\n\n get running(): boolean {\n return this.handle !== null;\n }\n\n /**\n * Starts the engine. Safe to call twice; the second call is a no-op, so that\n * library code can call it defensively.\n *\n * Not safe to call after `stop()`, and not because of anything here: Blink\n * starts once per process and cannot be restarted. Another engine means\n * another process.\n */\n start(options: StartOptions = {}): this {\n if (this.handle) {\n return this;\n }\n if (this.stopped) {\n throw new Error(\n 'shotium: this engine was stopped, and Blink cannot be started ' +\n 'again in a process that has already run it. Start another ' +\n 'process, or keep the engine up between screenshots.');\n }\n if (startedInThisProcess) {\n throw new Error(\n 'shotium: an engine has already run in this process. Blink is a ' +\n 'process-wide singleton -- there is one per process, ever -- so a ' +\n 'second Runtime cannot have one. Use the shared `runtime`, or run ' +\n 'another process.');\n }\n const native = binding.load();\n const resolved = resolveStartOptions(options);\n\n const engineOptions: Record<string, unknown> = {};\n if (resolved.cacheDir !== null) {\n engineOptions.cacheDir = resolved.cacheDir;\n }\n if (resolved.userAgent !== undefined) {\n engineOptions.userAgent = resolved.userAgent;\n }\n // The packs sit beside the library, and the library cannot find itself on\n // Linux -- the path the engine resolves for \"this module\" goes through\n // /proc/self/exe, which names node. Saying it here is cheaper than\n // teaching the engine a second way to look. See shot_api.h.\n engineOptions.resourceDir = resolved.resourceDir ?? binding.directory();\n\n this.handle = native.create(JSON.stringify(engineOptions));\n startedInThisProcess = true;\n return this;\n }\n\n /**\n * Stops the engine, after whatever is queued.\n *\n * Final for this process: see the note above. A program that will want\n * another screenshot later should leave the engine up and call `purge()`\n * instead, which hands back the memory without giving up the engine.\n */\n async stop(): Promise<void> {\n if (!this.handle) {\n return;\n }\n this.stopped = true;\n // After the queue, not before: destroy() waits for a capture in flight\n // anyway, and doing it in order means a caller's last screenshot resolves\n // rather than racing the shutdown.\n const handle = this.handle;\n this.handle = null;\n await this.tail.catch(() => {});\n binding.load().destroy(handle);\n }\n\n /**\n * Hands back what the engine is holding but can rebuild.\n * `releaseWorkingSet` additionally asks the OS for the pages, which the next\n * screenshot pays back in soft faults -- worth it when there may not be a\n * next one soon.\n *\n * The daemon does this for itself on a timer because it can watch its own\n * request stream go quiet. Here the queue belongs to the caller, so the\n * caller is the one who knows a batch has ended.\n */\n purge({releaseWorkingSet = false}: PurgeOptions = {}): void {\n if (!this.handle) {\n return;\n }\n binding.load().purge(this.handle, releaseWorkingSet);\n }\n\n /**\n * Renders one screenshot. Resolves to the encoded image, or to `null` when\n * `path` was given and the engine wrote the file itself.\n */\n // `async` and not a plain function returning capture()'s promise: toRequest()\n // throws, and a caller who wrote `screenshot(bad).catch(...)` would get the\n // throw past the catch and into the surrounding frame. The whole surface is\n // promise-shaped, so a bad request is a rejection like everything else.\n async screenshot(options: ScreenshotOptions): Promise<Buffer|null> {\n // Before anything else, and before the queue: a malformed request should\n // be a rejection now rather than one that waits its turn.\n return this.capture(toRequest(options));\n }\n\n /**\n * The same, for a request that is already in wire form.\n *\n * The daemon reads these off a socket, where they arrived having been\n * validated by the client that sent them. Re-deriving one from\n * ScreenshotOptions would mean the daemon validating a request it cannot see\n * the original of, and rejecting fields a newer client legitimately sent.\n */\n async capture(request: WireRequest): Promise<Buffer|null> {\n if (!this.handle) {\n this.start();\n }\n const handle = this.handle;\n const native = binding.load();\n\n // Chain onto the tail so that captures run one at a time. The catch keeps\n // one failure from poisoning everything queued behind it.\n const result = this.tail.catch(() => {}).then(\n () => native.capture(handle, JSON.stringify(request)));\n this.tail = result.catch(() => {});\n const image = await result;\n return request.path ? null : image;\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,137 @@
|
|
|
1
|
-
import { a as PageGotoParams, c as StartOptions, l as Viewport, n as DaemonOptions, o as PurgeOptions, r as DaemonStatus, s as ScreenshotOptions, t as Clip, u as WorkerEvent } from "./types-x9HtkzeE.js";
|
|
2
1
|
import { EventEmitter } from "node:events";
|
|
3
2
|
import net from "node:net";
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/** A region of the document, in CSS pixels. */
|
|
5
|
+
interface Clip {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
interface PageGotoParams {
|
|
12
|
+
/** Milliseconds before the load is abandoned. Default 30000. */
|
|
13
|
+
timeout?: number;
|
|
14
|
+
/**
|
|
15
|
+
* `load` waits for parsing to finish, the load event to fire and every
|
|
16
|
+
* request to complete. `networkidle` additionally waits for a 500ms window
|
|
17
|
+
* with nothing in flight, which matters for documents that keep fetching
|
|
18
|
+
* after the load event -- CSS that pulls in more CSS, or a font a late style
|
|
19
|
+
* change brought in.
|
|
20
|
+
*/
|
|
21
|
+
waitUntil?: 'load' | 'networkidle';
|
|
22
|
+
}
|
|
23
|
+
/** The viewport the document is laid out in. */
|
|
24
|
+
interface Viewport {
|
|
25
|
+
/** CSS pixels. Default 1280. */
|
|
26
|
+
width?: number;
|
|
27
|
+
/** CSS pixels. Default 720. */
|
|
28
|
+
height?: number;
|
|
29
|
+
}
|
|
30
|
+
interface ScreenshotOptions {
|
|
31
|
+
/** An http/https/file URL, or a local path. */
|
|
32
|
+
file: string;
|
|
33
|
+
/** Default `png`. */
|
|
34
|
+
type?: 'png' | 'jpeg' | 'webp';
|
|
35
|
+
/** Capture the whole document rather than the viewport. */
|
|
36
|
+
fullPage?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Capture the box of the first element matching this CSS selector. Resolved
|
|
39
|
+
* inside the renderer with Document::querySelector -- there is no JavaScript
|
|
40
|
+
* engine, so nothing is injected into the page.
|
|
41
|
+
*/
|
|
42
|
+
selector?: string;
|
|
43
|
+
/** 1-100, `jpeg` and `webp` only. Default 90. */
|
|
44
|
+
quality?: number;
|
|
45
|
+
/** Device scale factor, 0.01-8. Default 1. */
|
|
46
|
+
scale?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Keep the alpha channel instead of painting the page's white backdrop.
|
|
49
|
+
* Rejected for `jpeg`, which has no alpha channel.
|
|
50
|
+
*/
|
|
51
|
+
omitBackground?: boolean;
|
|
52
|
+
/** Write the image here instead of returning it, saving a round trip. */
|
|
53
|
+
path?: string;
|
|
54
|
+
pageGotoParams?: PageGotoParams;
|
|
55
|
+
/** A region of the document, in CSS pixels. */
|
|
56
|
+
clip?: Clip;
|
|
57
|
+
/** The viewport the document is laid out in. */
|
|
58
|
+
viewport?: Viewport;
|
|
59
|
+
/**
|
|
60
|
+
* Let the document read `file:` subresources. Off by default: a library does
|
|
61
|
+
* not get to decide for its caller that a document may read the filesystem it
|
|
62
|
+
* is rendered on.
|
|
63
|
+
*/
|
|
64
|
+
allowFileAccess?: boolean;
|
|
65
|
+
}
|
|
66
|
+
interface StartOptions {
|
|
67
|
+
/**
|
|
68
|
+
* Root of the HTTP disk cache. `null` disables caching entirely, which is
|
|
69
|
+
* the default: a program holding the engine is often short-lived, and a
|
|
70
|
+
* cache it never reads twice is a directory it leaves behind.
|
|
71
|
+
*/
|
|
72
|
+
cacheDir?: string | null;
|
|
73
|
+
/** Overrides the built-in user agent string. */
|
|
74
|
+
userAgent?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Where `shotium_data.pak` and `shotium_strings.pak` are. Defaults to the
|
|
77
|
+
* directory the engine was loaded from, which is where they ship.
|
|
78
|
+
*/
|
|
79
|
+
resourceDir?: string;
|
|
80
|
+
}
|
|
81
|
+
interface DaemonOptions extends StartOptions {
|
|
82
|
+
/**
|
|
83
|
+
* Address the daemon by name instead of by configuration. Without it the
|
|
84
|
+
* endpoint is a hash of `cacheDir`, `userAgent` and `resourceDir`, so a
|
|
85
|
+
* client never attaches to a daemon that renders with something other than
|
|
86
|
+
* what it asked for.
|
|
87
|
+
*/
|
|
88
|
+
name?: string;
|
|
89
|
+
/** The pipe or socket to use, overriding both the name and the hash. */
|
|
90
|
+
endpoint?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Exit after this long with no connections and nothing rendering. Default
|
|
93
|
+
* 300000; `0` never exits.
|
|
94
|
+
*/
|
|
95
|
+
idleTimeoutMs?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Render one throwaway document at startup, so the first real request does
|
|
98
|
+
* not pay for whatever the engine initialises lazily. Default true.
|
|
99
|
+
*/
|
|
100
|
+
prewarm?: boolean;
|
|
101
|
+
/** Fail instead of starting a daemon when none is listening. */
|
|
102
|
+
spawn?: boolean;
|
|
103
|
+
/** Where a spawned daemon's diagnostics go. Default `$SHOTIUM_DAEMON_LOG`. */
|
|
104
|
+
logFile?: string;
|
|
105
|
+
/** How long to wait for a daemon this process started to bind. */
|
|
106
|
+
startTimeoutMs?: number;
|
|
107
|
+
}
|
|
108
|
+
interface DaemonStatus {
|
|
109
|
+
ok?: boolean;
|
|
110
|
+
running?: boolean;
|
|
111
|
+
spawned?: boolean;
|
|
112
|
+
pid: number;
|
|
113
|
+
endpoint: string;
|
|
114
|
+
cacheDir: string | null;
|
|
115
|
+
userAgent?: string;
|
|
116
|
+
resourceDir?: string;
|
|
117
|
+
/** The engine has rendered at least once. */
|
|
118
|
+
warm: boolean;
|
|
119
|
+
uptimeMs: number;
|
|
120
|
+
connections: number;
|
|
121
|
+
inFlight: number;
|
|
122
|
+
served: number;
|
|
123
|
+
idleTimeoutMs: number;
|
|
124
|
+
version: string;
|
|
125
|
+
}
|
|
126
|
+
interface PurgeOptions {
|
|
127
|
+
/**
|
|
128
|
+
* Also ask the OS to take the engine's pages back. The next screenshot pays
|
|
129
|
+
* them back in soft page faults -- a few milliseconds -- so this is for when
|
|
130
|
+
* there may not be a next one soon.
|
|
131
|
+
*/
|
|
132
|
+
releaseWorkingSet?: boolean;
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
4
135
|
//#region src/lib/client.d.ts
|
|
5
136
|
interface ClientReply {
|
|
6
137
|
id: number;
|
|
@@ -36,7 +167,7 @@ declare class DaemonClient extends EventEmitter {
|
|
|
36
167
|
}
|
|
37
168
|
//#endregion
|
|
38
169
|
//#region src/index.d.ts
|
|
39
|
-
/** The five things a caller does with the resident
|
|
170
|
+
/** The five things a caller does with the resident engine. */
|
|
40
171
|
interface Daemon {
|
|
41
172
|
/** Connects, starting a daemon if none is listening. */
|
|
42
173
|
connect(options?: DaemonOptions): Promise<DaemonClient>;
|
|
@@ -57,69 +188,71 @@ interface Daemon {
|
|
|
57
188
|
endpoint: string;
|
|
58
189
|
}>;
|
|
59
190
|
}
|
|
60
|
-
interface Runtime {
|
|
61
|
-
on(event: 'ready', listener: (info: {
|
|
62
|
-
workers: number;
|
|
63
|
-
}) => void): this;
|
|
64
|
-
on(event: 'exit', listener: (event: WorkerEvent) => void): this;
|
|
65
|
-
on(event: 'crash', listener: (event: WorkerEvent) => void): this;
|
|
66
|
-
on(event: 'timeout', listener: (event: {
|
|
67
|
-
worker: number;
|
|
68
|
-
timeout: number;
|
|
69
|
-
}) => void): this;
|
|
70
|
-
on(event: 'worker-restart', listener: (event: {
|
|
71
|
-
worker: number;
|
|
72
|
-
reason: string;
|
|
73
|
-
delay: number;
|
|
74
|
-
}) => void): this;
|
|
75
|
-
/** A worker could not be started at all -- a missing or unusable binary. */
|
|
76
|
-
on(event: 'worker-error', listener: (event: {
|
|
77
|
-
worker: number;
|
|
78
|
-
error: Error;
|
|
79
|
-
}) => void): this;
|
|
80
|
-
on(event: 'stderr', listener: (event: {
|
|
81
|
-
worker: number;
|
|
82
|
-
line: string;
|
|
83
|
-
}) => void): this;
|
|
84
|
-
}
|
|
85
191
|
/**
|
|
86
|
-
* The
|
|
192
|
+
* The engine, and its lifecycle, in this process.
|
|
193
|
+
*
|
|
194
|
+
* import shotium from '@shotkit/shotium';
|
|
195
|
+
*
|
|
196
|
+
* shotium.runtime.start();
|
|
197
|
+
* const png = await shotium.screenshot({file: 'https://example.com'});
|
|
198
|
+
* await shotium.runtime.stop();
|
|
199
|
+
*
|
|
200
|
+
* `start` and `stop` are explicit because starting Blink is the expensive part
|
|
201
|
+
* -- tens of milliseconds and a working set that stays resident -- and only
|
|
202
|
+
* the caller knows whether the next screenshot is coming in a moment or never.
|
|
203
|
+
* Neither call is required: a screenshot starts the engine if it is not up.
|
|
204
|
+
* What they buy is control over when that cost is paid, and the certainty that
|
|
205
|
+
* it has been given back.
|
|
87
206
|
*
|
|
88
|
-
* `runtime` below is the singleton
|
|
89
|
-
*
|
|
90
|
-
*
|
|
207
|
+
* `runtime` below is the singleton because there is nothing else it could be:
|
|
208
|
+
* Blink starts once per process and cannot be restarted, so a second Runtime
|
|
209
|
+
* in the same process has no engine to have. Construct one directly only to
|
|
210
|
+
* own the lifecycle yourself instead of using `runtime`. Parallelism is more
|
|
211
|
+
* processes, not more Runtimes.
|
|
91
212
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
213
|
+
* `daemon` is the same engine in a process of its own, behind a socket, for
|
|
214
|
+
* callers whose own process does not live long enough to be worth starting
|
|
215
|
+
* one.
|
|
95
216
|
*/
|
|
96
|
-
declare class Runtime
|
|
97
|
-
private
|
|
217
|
+
declare class Runtime {
|
|
218
|
+
private engine;
|
|
98
219
|
get running(): boolean;
|
|
99
220
|
/**
|
|
100
|
-
* Starts the
|
|
101
|
-
* library code can call it defensively.
|
|
221
|
+
* Starts the engine. Safe to call twice; the second call is a no-op, so that
|
|
222
|
+
* library code can call it defensively. Not safe after `stop()` -- see there.
|
|
102
223
|
*
|
|
103
|
-
* Every option has a default
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
224
|
+
* Every option has a default. `cacheDir` is the HTTP disk cache and `null`
|
|
225
|
+
* disables it; `resourceDir` is where `shotium_data.pak` and
|
|
226
|
+
* `shotium_strings.pak` are, and defaults to the directory the engine was
|
|
227
|
+
* loaded from, which is where they ship.
|
|
107
228
|
*/
|
|
108
229
|
start(options?: StartOptions): this;
|
|
109
|
-
/**
|
|
230
|
+
/**
|
|
231
|
+
* Stops the engine, after whatever is queued.
|
|
232
|
+
*
|
|
233
|
+
* Final for this process. Blink writes process-wide state that it has no
|
|
234
|
+
* path to undo, so starting again -- here or on another Runtime -- throws
|
|
235
|
+
* rather than quietly handing back something that cannot render. A program
|
|
236
|
+
* that wants another screenshot later should stay started and `purge()`.
|
|
237
|
+
*/
|
|
110
238
|
stop(): Promise<void>;
|
|
239
|
+
/**
|
|
240
|
+
* Hands back what the engine is holding but can rebuild. Worth calling when
|
|
241
|
+
* a batch has ended and the next one may be a while away.
|
|
242
|
+
*/
|
|
243
|
+
purge(options?: PurgeOptions): void;
|
|
111
244
|
/**
|
|
112
245
|
* Renders one screenshot. Resolves to the encoded image, or to `null` when
|
|
113
|
-
* `path` was given and the
|
|
246
|
+
* `path` was given and the engine wrote the file itself.
|
|
114
247
|
*/
|
|
115
248
|
screenshot(options: ScreenshotOptions): Promise<Buffer | null>;
|
|
116
249
|
}
|
|
117
|
-
/** The shared
|
|
250
|
+
/** The shared engine: one per process, started on first use. */
|
|
118
251
|
declare const runtime: Runtime;
|
|
119
|
-
/** One screenshot through the shared
|
|
252
|
+
/** One screenshot through the shared engine, starting it if it is not up. */
|
|
120
253
|
declare const screenshot: (options: ScreenshotOptions) => Promise<Buffer | null>;
|
|
121
254
|
/**
|
|
122
|
-
* The resident
|
|
255
|
+
* The resident engine: a process that outlives the one that started it,
|
|
123
256
|
* reachable over a named pipe on Windows and a unix socket elsewhere. For
|
|
124
257
|
* callers that are short-lived themselves. See lib/daemon.ts.
|
|
125
258
|
*/
|
|
@@ -131,5 +264,5 @@ declare const _default: {
|
|
|
131
264
|
daemon: Daemon;
|
|
132
265
|
};
|
|
133
266
|
//#endregion
|
|
134
|
-
export { type Clip, Daemon, type DaemonClient, type DaemonOptions, type DaemonStatus, type PageGotoParams, type PurgeOptions, Runtime, type ScreenshotOptions, type StartOptions, type Viewport,
|
|
267
|
+
export { type Clip, Daemon, type DaemonClient, type DaemonOptions, type DaemonStatus, type PageGotoParams, type PurgeOptions, Runtime, type ScreenshotOptions, type StartOptions, type Viewport, daemon, _default as default, runtime, screenshot };
|
|
135
268
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { n as timeoutFor, r as toRequest, t as SUPERVISOR_MARGIN_MS } from "./request-qZXS3N9f.js";
|
|
3
|
-
import { EventEmitter } from "node:events";
|
|
1
|
+
import { a as encodeFrame, i as FrameReader, n as timeoutFor, o as endpointFor, r as toRequest, s as resolveStartOptions, t as Engine } from "./engine-Xe7nH-1i.js";
|
|
4
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
5
4
|
import fs from "node:fs";
|
|
6
5
|
import net from "node:net";
|
|
7
6
|
import path from "node:path";
|
|
@@ -89,12 +88,10 @@ var DaemonClient = class extends EventEmitter {
|
|
|
89
88
|
/** Resolves to the image, or to null when `path` was given. */
|
|
90
89
|
async screenshot(options) {
|
|
91
90
|
const request = toRequest(options);
|
|
92
|
-
const retry = typeof options.retry === "number" ? options.retry : 0;
|
|
93
91
|
return (await this.send({
|
|
94
92
|
op: "screenshot",
|
|
95
93
|
request,
|
|
96
|
-
timeout: timeoutFor(options)
|
|
97
|
-
retry
|
|
94
|
+
timeout: timeoutFor(options)
|
|
98
95
|
})).image;
|
|
99
96
|
}
|
|
100
97
|
async status() {
|
|
@@ -141,10 +138,9 @@ function resolveDaemonOptions(options = {}) {
|
|
|
141
138
|
}
|
|
142
139
|
function spawnDaemon(options) {
|
|
143
140
|
const config = {
|
|
144
|
-
binary: options.binary,
|
|
145
|
-
workers: options.workers,
|
|
146
141
|
cacheDir: options.cacheDir,
|
|
147
|
-
|
|
142
|
+
userAgent: options.userAgent,
|
|
143
|
+
resourceDir: options.resourceDir,
|
|
148
144
|
endpoint: options.endpoint,
|
|
149
145
|
idleTimeoutMs: options.idleTimeoutMs,
|
|
150
146
|
prewarm: options.prewarm
|
|
@@ -262,73 +258,81 @@ async function screenshot$1(options) {
|
|
|
262
258
|
//#endregion
|
|
263
259
|
//#region src/index.ts
|
|
264
260
|
/**
|
|
265
|
-
* The
|
|
261
|
+
* The engine, and its lifecycle, in this process.
|
|
262
|
+
*
|
|
263
|
+
* import shotium from '@shotkit/shotium';
|
|
264
|
+
*
|
|
265
|
+
* shotium.runtime.start();
|
|
266
|
+
* const png = await shotium.screenshot({file: 'https://example.com'});
|
|
267
|
+
* await shotium.runtime.stop();
|
|
266
268
|
*
|
|
267
|
-
* `
|
|
268
|
-
*
|
|
269
|
-
*
|
|
269
|
+
* `start` and `stop` are explicit because starting Blink is the expensive part
|
|
270
|
+
* -- tens of milliseconds and a working set that stays resident -- and only
|
|
271
|
+
* the caller knows whether the next screenshot is coming in a moment or never.
|
|
272
|
+
* Neither call is required: a screenshot starts the engine if it is not up.
|
|
273
|
+
* What they buy is control over when that cost is paid, and the certainty that
|
|
274
|
+
* it has been given back.
|
|
270
275
|
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
276
|
+
* `runtime` below is the singleton because there is nothing else it could be:
|
|
277
|
+
* Blink starts once per process and cannot be restarted, so a second Runtime
|
|
278
|
+
* in the same process has no engine to have. Construct one directly only to
|
|
279
|
+
* own the lifecycle yourself instead of using `runtime`. Parallelism is more
|
|
280
|
+
* processes, not more Runtimes.
|
|
281
|
+
*
|
|
282
|
+
* `daemon` is the same engine in a process of its own, behind a socket, for
|
|
283
|
+
* callers whose own process does not live long enough to be worth starting
|
|
284
|
+
* one.
|
|
274
285
|
*/
|
|
275
|
-
var Runtime = class
|
|
276
|
-
|
|
286
|
+
var Runtime = class {
|
|
287
|
+
engine = new Engine();
|
|
277
288
|
get running() {
|
|
278
|
-
return this.
|
|
289
|
+
return this.engine.running;
|
|
279
290
|
}
|
|
280
291
|
/**
|
|
281
|
-
* Starts the
|
|
282
|
-
* library code can call it defensively.
|
|
292
|
+
* Starts the engine. Safe to call twice; the second call is a no-op, so that
|
|
293
|
+
* library code can call it defensively. Not safe after `stop()` -- see there.
|
|
283
294
|
*
|
|
284
|
-
* Every option has a default
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
295
|
+
* Every option has a default. `cacheDir` is the HTTP disk cache and `null`
|
|
296
|
+
* disables it; `resourceDir` is where `shotium_data.pak` and
|
|
297
|
+
* `shotium_strings.pak` are, and defaults to the directory the engine was
|
|
298
|
+
* loaded from, which is where they ship.
|
|
288
299
|
*/
|
|
289
300
|
start(options = {}) {
|
|
290
|
-
|
|
291
|
-
const pool = new Pool(resolveStartOptions(options));
|
|
292
|
-
this.pool = pool;
|
|
293
|
-
for (const event of [
|
|
294
|
-
"ready",
|
|
295
|
-
"exit",
|
|
296
|
-
"crash",
|
|
297
|
-
"timeout",
|
|
298
|
-
"worker-restart",
|
|
299
|
-
"worker-error",
|
|
300
|
-
"stderr"
|
|
301
|
-
]) pool.on(event, (payload) => this.emit(event, payload));
|
|
302
|
-
pool.start();
|
|
301
|
+
this.engine.start(options);
|
|
303
302
|
return this;
|
|
304
303
|
}
|
|
305
|
-
/**
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Stops the engine, after whatever is queued.
|
|
306
|
+
*
|
|
307
|
+
* Final for this process. Blink writes process-wide state that it has no
|
|
308
|
+
* path to undo, so starting again -- here or on another Runtime -- throws
|
|
309
|
+
* rather than quietly handing back something that cannot render. A program
|
|
310
|
+
* that wants another screenshot later should stay started and `purge()`.
|
|
311
|
+
*/
|
|
312
|
+
stop() {
|
|
313
|
+
return this.engine.stop();
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Hands back what the engine is holding but can rebuild. Worth calling when
|
|
317
|
+
* a batch has ended and the next one may be a while away.
|
|
318
|
+
*/
|
|
319
|
+
purge(options = {}) {
|
|
320
|
+
this.engine.purge(options);
|
|
311
321
|
}
|
|
312
322
|
/**
|
|
313
323
|
* Renders one screenshot. Resolves to the encoded image, or to `null` when
|
|
314
|
-
* `path` was given and the
|
|
324
|
+
* `path` was given and the engine wrote the file itself.
|
|
315
325
|
*/
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (!this.pool) this.start();
|
|
319
|
-
const retry = typeof options.retry === "number" ? options.retry : 0;
|
|
320
|
-
return (await this.pool.submit(request, {
|
|
321
|
-
timeout: timeoutFor(options) + SUPERVISOR_MARGIN_MS,
|
|
322
|
-
retry
|
|
323
|
-
})).image;
|
|
326
|
+
screenshot(options) {
|
|
327
|
+
return this.engine.screenshot(options);
|
|
324
328
|
}
|
|
325
329
|
};
|
|
326
|
-
/** The shared
|
|
330
|
+
/** The shared engine: one per process, started on first use. */
|
|
327
331
|
const runtime = new Runtime();
|
|
328
|
-
/** One screenshot through the shared
|
|
332
|
+
/** One screenshot through the shared engine, starting it if it is not up. */
|
|
329
333
|
const screenshot = (options) => runtime.screenshot(options);
|
|
330
334
|
/**
|
|
331
|
-
* The resident
|
|
335
|
+
* The resident engine: a process that outlives the one that started it,
|
|
332
336
|
* reachable over a named pipe on Windows and a unix socket elsewhere. For
|
|
333
337
|
* callers that are short-lived themselves. See lib/daemon.ts.
|
|
334
338
|
*/
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["screenshot","client.connect","client.screenshot","client.start","client.status","client.stop"],"sources":["../src/lib/client.ts","../src/index.ts"],"sourcesContent":["import {spawn} from 'node:child_process';\nimport {EventEmitter} from 'node:events';\nimport fs from 'node:fs';\nimport net from 'node:net';\nimport path from 'node:path';\nimport {fileURLToPath} from 'node:url';\n\nimport type {\n DaemonOptions,\n DaemonStatus,\n ScreenshotOptions,\n} from '../types.js';\n\nimport {resolveStartOptions} from './config.js';\nimport {endpointFor} from './endpoint.js';\nimport {FrameReader, encodeFrame} from './protocol.js';\nimport {timeoutFor, toRequest} from './request.js';\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 detached daemon's entry point, which is a build output beside this one.\n// It is spawned as `node <path>`, so it has to be a file on disk with a name\n// that does not move -- see tsdown.config.ts, where it is an entry of its own\n// for exactly that reason.\nconst DAEMON_MAIN = path.join(HERE, 'daemon_main.js');\n// How long to wait for a daemon this process just started to bind its\n// endpoint. Binding happens after the workers are spawned but before they are\n// warm, so this covers process startup and nothing else.\nconst START_TIMEOUT_MS = 20000;\nconst CONNECT_RETRY_MS = 20;\n\ninterface ClientReply {\n id: number;\n ok?: boolean;\n error?: string;\n path?: string;\n}\n\ninterface ClientResult {\n header: ClientReply;\n image: Buffer|null;\n}\n\ninterface Pending {\n resolve: (result: ClientResult) => void;\n reject: (error: Error) => void;\n}\n\ninterface ResolvedDaemonOptions {\n binary: string;\n workers: number;\n cacheDir: string|null;\n args: string[];\n name: string|undefined;\n endpoint: string;\n idleTimeoutMs: number|undefined;\n prewarm: boolean|undefined;\n logFile: string|null;\n}\n\n// The client half of the resident daemon.\n//\n// One connection can carry several requests at once, which is the difference\n// between this and the worker protocol underneath: every message carries an\n// `id` and the answers are matched back by it, so a caller can fire ten\n// screenshots down one socket and let the pool on the other side spread them\n// across workers.\nclass DaemonClient extends EventEmitter {\n private readonly socket: net.Socket;\n private readonly endpointPath: string;\n private readonly pending = new Map<number, Pending>();\n private nextId = 1;\n private header: ClientReply|null = null;\n private reader = new FrameReader();\n\n constructor(socket: net.Socket, endpoint: string) {\n super();\n this.socket = socket;\n this.endpointPath = endpoint;\n\n socket.on('data', (chunk: Buffer) => this.onData(chunk));\n socket.on('error', (error: Error) => this.failAll(error));\n socket.on('close', () => {\n this.failAll(new Error('shotium: the daemon closed the connection'));\n this.emit('close', {});\n });\n }\n\n get endpoint(): string {\n return this.endpointPath;\n }\n\n get closed(): boolean {\n return this.socket.destroyed;\n }\n\n private onData(chunk: Buffer): void {\n this.reader.push(chunk);\n for (;;) {\n const frame = this.reader.next();\n if (frame === null) {\n return;\n }\n if (this.header === null) {\n try {\n this.header = JSON.parse(frame.toString('utf8')) as ClientReply;\n } catch {\n this.failAll(\n new Error('shotium: the daemon sent a header that is not JSON'));\n return;\n }\n continue;\n }\n const header = this.header;\n this.header = null;\n this.settle(header, frame);\n }\n }\n\n private settle(header: ClientReply, payload: Buffer): void {\n const pending = this.pending.get(header.id);\n if (!pending) {\n return;\n }\n this.pending.delete(header.id);\n if (header.ok) {\n pending.resolve({header, image: header.path ? null : payload});\n } else {\n pending.reject(new Error(header.error || 'shotium: request failed'));\n }\n }\n\n private failAll(error: Error): void {\n for (const [, pending] of this.pending) {\n pending.reject(error);\n }\n this.pending.clear();\n }\n\n // Sends one message and resolves with {header, image}.\n send(message: Record<string, unknown>): Promise<ClientResult> {\n return new Promise<ClientResult>((resolve, reject) => {\n if (this.socket.destroyed) {\n reject(new Error('shotium: not connected to a daemon'));\n return;\n }\n const id = this.nextId++;\n this.pending.set(id, {resolve, reject});\n this.socket.write(\n encodeFrame(Buffer.from(JSON.stringify({...message, id}), 'utf8')));\n });\n }\n\n /** Resolves to the image, or to null when `path` was given. */\n async screenshot(options: ScreenshotOptions): Promise<Buffer|null> {\n const request = toRequest(options);\n const retry = typeof options.retry === 'number' ? options.retry : 0;\n const result = await this.send({\n op: 'screenshot',\n request,\n timeout: timeoutFor(options),\n retry,\n });\n return result.image;\n }\n\n async status(): Promise<DaemonStatus> {\n const {header} = await this.send({op: 'status'});\n return header as unknown as DaemonStatus;\n }\n\n async shutdown(): Promise<{ok: boolean}> {\n const {header} = await this.send({op: 'shutdown'});\n return {ok: header.ok === true};\n }\n\n close(): void {\n this.socket.end();\n this.socket.destroy();\n }\n}\n\n// Opens a connection to a daemon that is already listening, and fails if there\n// is not one. Nothing is spawned here: a caller that wants a daemon started\n// says so, because starting one is a side effect on the machine and not the\n// sort of thing a status query should do.\nfunction connectOnly(endpoint: string): Promise<DaemonClient> {\n return new Promise<DaemonClient>((resolve, reject) => {\n const socket = net.connect(endpoint);\n const onError = (error: Error) => {\n socket.destroy();\n reject(error);\n };\n socket.once('error', onError);\n socket.once('connect', () => {\n socket.removeListener('error', onError);\n resolve(new DaemonClient(socket, endpoint));\n });\n });\n}\n\nfunction resolveDaemonOptions(options: DaemonOptions = {}):\n ResolvedDaemonOptions {\n const resolved = resolveStartOptions(options);\n return {\n ...resolved,\n name: options.name,\n endpoint: endpointFor({\n ...resolved,\n name: options.name,\n endpoint: options.endpoint,\n }),\n idleTimeoutMs: options.idleTimeoutMs,\n prewarm: options.prewarm,\n logFile: options.logFile || process.env.SHOTIUM_DAEMON_LOG || null,\n };\n}\n\nfunction spawnDaemon(options: ResolvedDaemonOptions): void {\n const config = {\n binary: options.binary,\n workers: options.workers,\n cacheDir: options.cacheDir,\n args: options.args,\n endpoint: options.endpoint,\n idleTimeoutMs: options.idleTimeoutMs,\n prewarm: options.prewarm,\n };\n const encoded =\n Buffer.from(JSON.stringify(config), 'utf8').toString('base64');\n\n // Detached, with the standard streams let go of: the daemon has to outlive\n // the process that started it, and a child still holding this process's pipes\n // would keep it from exiting -- the exact failure that makes a \"background\"\n // daemon hang a shell.\n let stdio: 'ignore'|['ignore', number, number] = 'ignore';\n let logFd: number|null = null;\n if (options.logFile) {\n logFd = fs.openSync(options.logFile, 'a');\n stdio = ['ignore', logFd, logFd];\n }\n const child = spawn(process.execPath, [DAEMON_MAIN, encoded], {\n detached: true,\n stdio,\n windowsHide: true,\n });\n child.unref();\n if (logFd !== null) {\n fs.closeSync(logFd);\n }\n}\n\nconst sleep = (ms: number) =>\n new Promise<void>((resolve) => setTimeout(resolve, ms));\n\nexport interface EnsuredClient {\n client: DaemonClient;\n spawned: boolean;\n endpoint: string;\n}\n\n// Connects, starting a daemon if none answers.\n//\n// The endpoint existing is the readiness signal, so this is a connect loop\n// rather than a handshake: a daemon that has bound can be talked to, and one\n// that has not is indistinguishable from one that was never started. Several\n// processes racing here is fine -- the losers' daemons exit on EADDRINUSE and\n// everyone ends up on the winner.\nasync function ensureClient(options: DaemonOptions = {}):\n Promise<EnsuredClient> {\n const resolved = resolveDaemonOptions(options);\n try {\n const client = await connectOnly(resolved.endpoint);\n return {client, spawned: false, endpoint: resolved.endpoint};\n } catch {\n if (options.spawn === false) {\n throw new Error(`shotium: no daemon at ${resolved.endpoint}`);\n }\n }\n\n spawnDaemon(resolved);\n const deadline = Date.now() +\n (options.startTimeoutMs === undefined ? START_TIMEOUT_MS :\n options.startTimeoutMs);\n for (;;) {\n try {\n const client = await connectOnly(resolved.endpoint);\n return {client, spawned: true, endpoint: resolved.endpoint};\n } catch {\n if (Date.now() >= deadline) {\n throw new Error(\n `shotium: the daemon did not come up at ${resolved.endpoint}`);\n }\n await sleep(CONNECT_RETRY_MS);\n }\n }\n}\n\n// The five things a caller does with a daemon. Each opens a connection, does\n// one thing and closes it, which is the shape a short-lived process wants; a\n// service that will send more than one request calls connect() and keeps the\n// client.\nasync function connect(options: DaemonOptions = {}): Promise<DaemonClient> {\n const {client} = await ensureClient(options);\n return client;\n}\n\nasync function start(options: DaemonOptions = {}):\n Promise<DaemonStatus&{spawned: boolean}> {\n const {client, spawned, endpoint} = await ensureClient(options);\n try {\n const status = await client.status();\n return {...status, endpoint, spawned};\n } finally {\n client.close();\n }\n}\n\nasync function status(options: DaemonOptions = {}):\n Promise<Partial<DaemonStatus>&{running: boolean, endpoint: string}> {\n const resolved = resolveDaemonOptions(options);\n let client: DaemonClient;\n try {\n client = await connectOnly(resolved.endpoint);\n } catch {\n return {running: false, endpoint: resolved.endpoint};\n }\n try {\n return {...(await client.status()), running: true};\n } finally {\n client.close();\n }\n}\n\nasync function stop(options: DaemonOptions = {}):\n Promise<{stopped: boolean, endpoint: string}> {\n const resolved = resolveDaemonOptions(options);\n let client: DaemonClient;\n try {\n client = await connectOnly(resolved.endpoint);\n } catch {\n return {stopped: false, endpoint: resolved.endpoint};\n }\n try {\n await client.shutdown();\n return {stopped: true, endpoint: resolved.endpoint};\n } finally {\n client.close();\n }\n}\n\n// One screenshot through the daemon, connection and all. `daemon` carries the\n// pool's configuration -- binary, workers, cache root -- and is stripped out\n// here rather than sent, because it says which daemon to talk to and not what\n// to photograph.\nasync function screenshot(options: ScreenshotOptions&{daemon?: DaemonOptions}):\n Promise<Buffer|null> {\n const {daemon, ...rest} = options;\n const client = await connect(daemon || {});\n try {\n return await client.screenshot(rest);\n } finally {\n client.close();\n }\n}\n\nexport {\n DaemonClient,\n connect,\n ensureClient,\n resolveDaemonOptions,\n screenshot,\n start,\n status,\n stop,\n};\n","import {EventEmitter} from 'node:events';\n\nimport * as client from './lib/client.js';\nimport type {DaemonClient} from './lib/client.js';\nimport {resolveStartOptions} from './lib/config.js';\nimport {Pool} from './lib/pool.js';\nimport {SUPERVISOR_MARGIN_MS, timeoutFor, toRequest} from './lib/request.js';\nimport type {\n DaemonOptions,\n DaemonStatus,\n ScreenshotOptions,\n StartOptions,\n WorkerEvent,\n} from './types.js';\n\nexport type {\n Clip,\n DaemonOptions,\n DaemonStatus,\n PageGotoParams,\n PurgeOptions,\n ScreenshotOptions,\n StartOptions,\n Viewport,\n WorkerEvent,\n} from './types.js';\nexport type {DaemonClient} from './lib/client.js';\n\n/** The five things a caller does with the resident pool. */\nexport interface Daemon {\n /** Connects, starting a daemon if none is listening. */\n connect(options?: DaemonOptions): Promise<DaemonClient>;\n /** One screenshot through the daemon, connection and all. */\n screenshot(options: ScreenshotOptions&{daemon?: DaemonOptions}):\n Promise<Buffer|null>;\n /** Starts one if it is not up, and reports what is there either way. */\n start(options?: DaemonOptions): Promise<DaemonStatus&{spawned: boolean}>;\n status(options?: DaemonOptions):\n Promise<Partial<DaemonStatus>&{running: boolean, endpoint: string}>;\n stop(options?: DaemonOptions): Promise<{stopped: boolean, endpoint: string}>;\n}\n\n// The events the pool forwards, and the only ones. Declared as an interface\n// merged into the class below rather than as a catch-all `on(string, ...)`,\n// so that a listener for an event this runtime never emits is a compile error\n// rather than a callback nobody ever calls.\nexport interface Runtime {\n on(event: 'ready', listener: (info: {workers: number}) => void): this;\n on(event: 'exit', listener: (event: WorkerEvent) => void): this;\n on(event: 'crash', listener: (event: WorkerEvent) => void): this;\n on(event: 'timeout',\n listener: (event: {worker: number, timeout: number}) => void): this;\n on(event: 'worker-restart',\n listener: (event: {worker: number, reason: string, delay: number}) => void):\n this;\n /** A worker could not be started at all -- a missing or unusable binary. */\n on(event: 'worker-error',\n listener: (event: {worker: number, error: Error}) => void): this;\n on(event: 'stderr',\n listener: (event: {worker: number, line: string}) => void): this;\n}\n\n/**\n * The library's one runtime: a pool of worker processes plus its lifecycle.\n *\n * `runtime` below is the singleton, because the expensive part is the\n * processes and a second runtime would double them for no gain. Anyone who\n * genuinely wants two constructs a Runtime directly.\n *\n * Its pool lives and dies with this process. `daemon` is the same pool behind\n * a socket, for callers whose process does not live long enough to be worth\n * starting one.\n */\nexport class Runtime extends EventEmitter {\n private pool: Pool|null = null;\n\n get running(): boolean {\n return this.pool !== null;\n }\n\n /**\n * Starts the pool. Safe to call twice; the second call is a no-op, so that\n * library code can call it defensively.\n *\n * Every option has a default: the binary is `$SHOTIUM_BINARY`, then the\n * platform package, then `./bin/shotium.exe`; the worker count is half the\n * cores, at least one and at most four; the cache root is a directory under\n * the system temp, and `null` disables caching.\n */\n start(options: StartOptions = {}): this {\n if (this.pool) {\n return this;\n }\n const pool = new Pool(resolveStartOptions(options));\n this.pool = pool;\n for (const event\n of ['ready', 'exit', 'crash', 'timeout', 'worker-restart',\n 'worker-error', 'stderr']) {\n pool.on(event, (payload) => this.emit(event, payload));\n }\n pool.start();\n return this;\n }\n\n /** Stops every worker. The pool can be started again afterwards. */\n async stop(): Promise<void> {\n if (!this.pool) {\n return;\n }\n const pool = this.pool;\n this.pool = null;\n await pool.stop();\n }\n\n /**\n * Renders one screenshot. Resolves to the encoded image, or to `null` when\n * `path` was given and the worker wrote the file itself.\n */\n async screenshot(options: ScreenshotOptions): Promise<Buffer|null> {\n // Validate before starting anything. A malformed request should not cost a\n // pool of worker processes to discover, and toRequest() is the only check\n // that can be made without one.\n const request = toRequest(options);\n if (!this.pool) {\n this.start();\n }\n const retry = typeof options.retry === 'number' ? options.retry : 0;\n const result = await this.pool!.submit(request, {\n timeout: timeoutFor(options) + SUPERVISOR_MARGIN_MS,\n retry,\n });\n return result.image;\n }\n}\n\n/** The shared pool: one per process, started on first use. */\nconst runtime = new Runtime();\n\n/** One screenshot through the shared pool, starting it if it is not up. */\nconst screenshot = (options: ScreenshotOptions): Promise<Buffer|null> =>\n runtime.screenshot(options);\n\n/**\n * The resident pool: workers that outlive the process that started them,\n * reachable over a named pipe on Windows and a unix socket elsewhere. For\n * callers that are short-lived themselves. See lib/daemon.ts.\n */\nconst daemon: Daemon = {\n connect: client.connect,\n screenshot: client.screenshot,\n start: client.start,\n status: client.status,\n stop: client.stop,\n};\n\nexport {runtime, screenshot, daemon};\n\n// A default as well as the names, because `import shotium from` is what a\n// caller coming from `require` writes first, and the two have to be the same\n// object rather than two views that drift.\nexport default {Runtime, runtime, screenshot, daemon};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["screenshot","client.connect","client.screenshot","client.start","client.status","client.stop"],"sources":["../src/lib/client.ts","../src/index.ts"],"sourcesContent":["import {spawn} from 'node:child_process';\nimport {EventEmitter} from 'node:events';\nimport fs from 'node:fs';\nimport net from 'node:net';\nimport path from 'node:path';\nimport {fileURLToPath} from 'node:url';\n\nimport type {\n DaemonOptions,\n DaemonStatus,\n ScreenshotOptions,\n} from '../types.js';\n\nimport {resolveStartOptions} from './config.js';\nimport {endpointFor} from './endpoint.js';\nimport {FrameReader, encodeFrame} from './protocol.js';\nimport {timeoutFor, toRequest} from './request.js';\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 detached daemon's entry point, which is a build output beside this one.\n// It is spawned as `node <path>`, so it has to be a file on disk with a name\n// that does not move -- see tsdown.config.ts, where it is an entry of its own\n// for exactly that reason.\nconst DAEMON_MAIN = path.join(HERE, 'daemon_main.js');\n// How long to wait for a daemon this process just started to bind its\n// endpoint. Binding happens after the workers are spawned but before they are\n// warm, so this covers process startup and nothing else.\nconst START_TIMEOUT_MS = 20000;\nconst CONNECT_RETRY_MS = 20;\n\ninterface ClientReply {\n id: number;\n ok?: boolean;\n error?: string;\n path?: string;\n}\n\ninterface ClientResult {\n header: ClientReply;\n image: Buffer|null;\n}\n\ninterface Pending {\n resolve: (result: ClientResult) => void;\n reject: (error: Error) => void;\n}\n\ninterface ResolvedDaemonOptions {\n cacheDir: string|null;\n userAgent?: string;\n resourceDir?: string;\n name: string|undefined;\n endpoint: string;\n idleTimeoutMs: number|undefined;\n prewarm: boolean|undefined;\n logFile: string|null;\n}\n\n// The client half of the resident daemon.\n//\n// One connection can carry several requests at once: every message carries an\n// `id` and the answers are matched back by it, so a caller can fire ten\n// screenshots down one socket without waiting between them. They still come\n// back one at a time -- there is one renderer on the other side -- so this\n// saves the round trips, not the renders.\nclass DaemonClient extends EventEmitter {\n private readonly socket: net.Socket;\n private readonly endpointPath: string;\n private readonly pending = new Map<number, Pending>();\n private nextId = 1;\n private header: ClientReply|null = null;\n private reader = new FrameReader();\n\n constructor(socket: net.Socket, endpoint: string) {\n super();\n this.socket = socket;\n this.endpointPath = endpoint;\n\n socket.on('data', (chunk: Buffer) => this.onData(chunk));\n socket.on('error', (error: Error) => this.failAll(error));\n socket.on('close', () => {\n this.failAll(new Error('shotium: the daemon closed the connection'));\n this.emit('close', {});\n });\n }\n\n get endpoint(): string {\n return this.endpointPath;\n }\n\n get closed(): boolean {\n return this.socket.destroyed;\n }\n\n private onData(chunk: Buffer): void {\n this.reader.push(chunk);\n for (;;) {\n const frame = this.reader.next();\n if (frame === null) {\n return;\n }\n if (this.header === null) {\n try {\n this.header = JSON.parse(frame.toString('utf8')) as ClientReply;\n } catch {\n this.failAll(\n new Error('shotium: the daemon sent a header that is not JSON'));\n return;\n }\n continue;\n }\n const header = this.header;\n this.header = null;\n this.settle(header, frame);\n }\n }\n\n private settle(header: ClientReply, payload: Buffer): void {\n const pending = this.pending.get(header.id);\n if (!pending) {\n return;\n }\n this.pending.delete(header.id);\n if (header.ok) {\n pending.resolve({header, image: header.path ? null : payload});\n } else {\n pending.reject(new Error(header.error || 'shotium: request failed'));\n }\n }\n\n private failAll(error: Error): void {\n for (const [, pending] of this.pending) {\n pending.reject(error);\n }\n this.pending.clear();\n }\n\n // Sends one message and resolves with {header, image}.\n send(message: Record<string, unknown>): Promise<ClientResult> {\n return new Promise<ClientResult>((resolve, reject) => {\n if (this.socket.destroyed) {\n reject(new Error('shotium: not connected to a daemon'));\n return;\n }\n const id = this.nextId++;\n this.pending.set(id, {resolve, reject});\n this.socket.write(\n encodeFrame(Buffer.from(JSON.stringify({...message, id}), 'utf8')));\n });\n }\n\n /** Resolves to the image, or to null when `path` was given. */\n async screenshot(options: ScreenshotOptions): Promise<Buffer|null> {\n const request = toRequest(options);\n const result = await this.send({\n op: 'screenshot',\n request,\n timeout: timeoutFor(options),\n });\n return result.image;\n }\n\n async status(): Promise<DaemonStatus> {\n const {header} = await this.send({op: 'status'});\n return header as unknown as DaemonStatus;\n }\n\n async shutdown(): Promise<{ok: boolean}> {\n const {header} = await this.send({op: 'shutdown'});\n return {ok: header.ok === true};\n }\n\n close(): void {\n this.socket.end();\n this.socket.destroy();\n }\n}\n\n// Opens a connection to a daemon that is already listening, and fails if there\n// is not one. Nothing is spawned here: a caller that wants a daemon started\n// says so, because starting one is a side effect on the machine and not the\n// sort of thing a status query should do.\nfunction connectOnly(endpoint: string): Promise<DaemonClient> {\n return new Promise<DaemonClient>((resolve, reject) => {\n const socket = net.connect(endpoint);\n const onError = (error: Error) => {\n socket.destroy();\n reject(error);\n };\n socket.once('error', onError);\n socket.once('connect', () => {\n socket.removeListener('error', onError);\n resolve(new DaemonClient(socket, endpoint));\n });\n });\n}\n\nfunction resolveDaemonOptions(options: DaemonOptions = {}):\n ResolvedDaemonOptions {\n const resolved = resolveStartOptions(options);\n return {\n ...resolved,\n name: options.name,\n endpoint: endpointFor({\n ...resolved,\n name: options.name,\n endpoint: options.endpoint,\n }),\n idleTimeoutMs: options.idleTimeoutMs,\n prewarm: options.prewarm,\n logFile: options.logFile || process.env.SHOTIUM_DAEMON_LOG || null,\n };\n}\n\nfunction spawnDaemon(options: ResolvedDaemonOptions): void {\n const config = {\n cacheDir: options.cacheDir,\n userAgent: options.userAgent,\n resourceDir: options.resourceDir,\n endpoint: options.endpoint,\n idleTimeoutMs: options.idleTimeoutMs,\n prewarm: options.prewarm,\n };\n const encoded =\n Buffer.from(JSON.stringify(config), 'utf8').toString('base64');\n\n // Detached, with the standard streams let go of: the daemon has to outlive\n // the process that started it, and a child still holding this process's pipes\n // would keep it from exiting -- the exact failure that makes a \"background\"\n // daemon hang a shell.\n let stdio: 'ignore'|['ignore', number, number] = 'ignore';\n let logFd: number|null = null;\n if (options.logFile) {\n logFd = fs.openSync(options.logFile, 'a');\n stdio = ['ignore', logFd, logFd];\n }\n const child = spawn(process.execPath, [DAEMON_MAIN, encoded], {\n detached: true,\n stdio,\n windowsHide: true,\n });\n child.unref();\n if (logFd !== null) {\n fs.closeSync(logFd);\n }\n}\n\nconst sleep = (ms: number) =>\n new Promise<void>((resolve) => setTimeout(resolve, ms));\n\nexport interface EnsuredClient {\n client: DaemonClient;\n spawned: boolean;\n endpoint: string;\n}\n\n// Connects, starting a daemon if none answers.\n//\n// The endpoint existing is the readiness signal, so this is a connect loop\n// rather than a handshake: a daemon that has bound can be talked to, and one\n// that has not is indistinguishable from one that was never started. Several\n// processes racing here is fine -- the losers' daemons exit on EADDRINUSE and\n// everyone ends up on the winner.\nasync function ensureClient(options: DaemonOptions = {}):\n Promise<EnsuredClient> {\n const resolved = resolveDaemonOptions(options);\n try {\n const client = await connectOnly(resolved.endpoint);\n return {client, spawned: false, endpoint: resolved.endpoint};\n } catch {\n if (options.spawn === false) {\n throw new Error(`shotium: no daemon at ${resolved.endpoint}`);\n }\n }\n\n spawnDaemon(resolved);\n const deadline = Date.now() +\n (options.startTimeoutMs === undefined ? START_TIMEOUT_MS :\n options.startTimeoutMs);\n for (;;) {\n try {\n const client = await connectOnly(resolved.endpoint);\n return {client, spawned: true, endpoint: resolved.endpoint};\n } catch {\n if (Date.now() >= deadline) {\n throw new Error(\n `shotium: the daemon did not come up at ${resolved.endpoint}`);\n }\n await sleep(CONNECT_RETRY_MS);\n }\n }\n}\n\n// The five things a caller does with a daemon. Each opens a connection, does\n// one thing and closes it, which is the shape a short-lived process wants; a\n// service that will send more than one request calls connect() and keeps the\n// client.\nasync function connect(options: DaemonOptions = {}): Promise<DaemonClient> {\n const {client} = await ensureClient(options);\n return client;\n}\n\nasync function start(options: DaemonOptions = {}):\n Promise<DaemonStatus&{spawned: boolean}> {\n const {client, spawned, endpoint} = await ensureClient(options);\n try {\n const status = await client.status();\n return {...status, endpoint, spawned};\n } finally {\n client.close();\n }\n}\n\nasync function status(options: DaemonOptions = {}):\n Promise<Partial<DaemonStatus>&{running: boolean, endpoint: string}> {\n const resolved = resolveDaemonOptions(options);\n let client: DaemonClient;\n try {\n client = await connectOnly(resolved.endpoint);\n } catch {\n return {running: false, endpoint: resolved.endpoint};\n }\n try {\n return {...(await client.status()), running: true};\n } finally {\n client.close();\n }\n}\n\nasync function stop(options: DaemonOptions = {}):\n Promise<{stopped: boolean, endpoint: string}> {\n const resolved = resolveDaemonOptions(options);\n let client: DaemonClient;\n try {\n client = await connectOnly(resolved.endpoint);\n } catch {\n return {stopped: false, endpoint: resolved.endpoint};\n }\n try {\n await client.shutdown();\n return {stopped: true, endpoint: resolved.endpoint};\n } finally {\n client.close();\n }\n}\n\n// One screenshot through the daemon, connection and all. `daemon` carries the\n// pool's configuration -- binary, workers, cache root -- and is stripped out\n// here rather than sent, because it says which daemon to talk to and not what\n// to photograph.\nasync function screenshot(options: ScreenshotOptions&{daemon?: DaemonOptions}):\n Promise<Buffer|null> {\n const {daemon, ...rest} = options;\n const client = await connect(daemon || {});\n try {\n return await client.screenshot(rest);\n } finally {\n client.close();\n }\n}\n\nexport {\n DaemonClient,\n connect,\n ensureClient,\n resolveDaemonOptions,\n screenshot,\n start,\n status,\n stop,\n};\n","import * as client from './lib/client.js';\nimport type {DaemonClient} from './lib/client.js';\nimport {Engine} from './lib/engine.js';\nimport type {\n DaemonOptions,\n DaemonStatus,\n PurgeOptions,\n ScreenshotOptions,\n StartOptions,\n} from './types.js';\n\nexport type {\n Clip,\n DaemonOptions,\n DaemonStatus,\n PageGotoParams,\n PurgeOptions,\n ScreenshotOptions,\n StartOptions,\n Viewport,\n} from './types.js';\nexport type {DaemonClient} from './lib/client.js';\n\n/** The five things a caller does with the resident engine. */\nexport interface Daemon {\n /** Connects, starting a daemon if none is listening. */\n connect(options?: DaemonOptions): Promise<DaemonClient>;\n /** One screenshot through the daemon, connection and all. */\n screenshot(options: ScreenshotOptions&{daemon?: DaemonOptions}):\n Promise<Buffer|null>;\n /** Starts one if it is not up, and reports what is there either way. */\n start(options?: DaemonOptions): Promise<DaemonStatus&{spawned: boolean}>;\n status(options?: DaemonOptions):\n Promise<Partial<DaemonStatus>&{running: boolean, endpoint: string}>;\n stop(options?: DaemonOptions): Promise<{stopped: boolean, endpoint: string}>;\n}\n\n/**\n * The engine, and its lifecycle, in this process.\n *\n * import shotium from '@shotkit/shotium';\n *\n * shotium.runtime.start();\n * const png = await shotium.screenshot({file: 'https://example.com'});\n * await shotium.runtime.stop();\n *\n * `start` and `stop` are explicit because starting Blink is the expensive part\n * -- tens of milliseconds and a working set that stays resident -- and only\n * the caller knows whether the next screenshot is coming in a moment or never.\n * Neither call is required: a screenshot starts the engine if it is not up.\n * What they buy is control over when that cost is paid, and the certainty that\n * it has been given back.\n *\n * `runtime` below is the singleton because there is nothing else it could be:\n * Blink starts once per process and cannot be restarted, so a second Runtime\n * in the same process has no engine to have. Construct one directly only to\n * own the lifecycle yourself instead of using `runtime`. Parallelism is more\n * processes, not more Runtimes.\n *\n * `daemon` is the same engine in a process of its own, behind a socket, for\n * callers whose own process does not live long enough to be worth starting\n * one.\n */\nexport class Runtime {\n private engine = new Engine();\n\n get running(): boolean {\n return this.engine.running;\n }\n\n /**\n * Starts the engine. Safe to call twice; the second call is a no-op, so that\n * library code can call it defensively. Not safe after `stop()` -- see there.\n *\n * Every option has a default. `cacheDir` is the HTTP disk cache and `null`\n * disables it; `resourceDir` is where `shotium_data.pak` and\n * `shotium_strings.pak` are, and defaults to the directory the engine was\n * loaded from, which is where they ship.\n */\n start(options: StartOptions = {}): this {\n this.engine.start(options);\n return this;\n }\n\n /**\n * Stops the engine, after whatever is queued.\n *\n * Final for this process. Blink writes process-wide state that it has no\n * path to undo, so starting again -- here or on another Runtime -- throws\n * rather than quietly handing back something that cannot render. A program\n * that wants another screenshot later should stay started and `purge()`.\n */\n stop(): Promise<void> {\n return this.engine.stop();\n }\n\n /**\n * Hands back what the engine is holding but can rebuild. Worth calling when\n * a batch has ended and the next one may be a while away.\n */\n purge(options: PurgeOptions = {}): void {\n this.engine.purge(options);\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 screenshot(options: ScreenshotOptions): Promise<Buffer|null> {\n return this.engine.screenshot(options);\n }\n}\n\n/** The shared engine: one per process, started on first use. */\nconst runtime = new Runtime();\n\n/** One screenshot through the shared engine, starting it if it is not up. */\nconst screenshot = (options: ScreenshotOptions): Promise<Buffer|null> =>\n runtime.screenshot(options);\n\n/**\n * The resident engine: a process that outlives the one that started it,\n * reachable over a named pipe on Windows and a unix socket elsewhere. For\n * callers that are short-lived themselves. See lib/daemon.ts.\n */\nconst daemon: Daemon = {\n connect: client.connect,\n screenshot: client.screenshot,\n start: client.start,\n status: client.status,\n stop: client.stop,\n};\n\nexport {runtime, screenshot, daemon};\n\n// A default as well as the names, because `import shotium from` is what a\n// caller coming from `require` writes first, and the two have to be the same\n// object rather than two views that drift.\nexport default {Runtime, runtime, screenshot, daemon};\n"],"mappings":"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"}
|