@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
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pool-BSgS6vkr.js","names":["platform.packageDir","platform.binaryName"],"sources":["../src/lib/config.ts","../src/lib/endpoint.ts","../src/lib/protocol.ts","../src/lib/worker.ts","../src/lib/pool.ts"],"sourcesContent":["import os from 'node:os';\nimport path from 'node:path';\nimport {fileURLToPath} from 'node:url';\n\nimport type {StartOptions} from '../types.js';\n\nimport * as platform from './platform.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// 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 binary: string;\n workers: number;\n cacheDir: string|null;\n args: 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 pool next to the first one that was already warm. See\n// endpoint.ts.\n//\n// Three places, in the order a caller means them: what they said, what npm\n// installed, and what they unpacked by hand. The middle one is the normal case\n// and the only one that needs no instructions.\nfunction defaultBinary(): string {\n if (process.env.SHOTIUM_BINARY) {\n return process.env.SHOTIUM_BINARY;\n }\n const dir = platform.packageDir();\n if (dir) {\n return path.join(dir, platform.binaryName());\n }\n // No platform package: an archive from the releases page, unpacked into\n // bin/ beside this file. This is also the path a checkout takes, where\n // nothing was installed from a registry at all.\n return path.join(HERE, '..', 'bin', platform.binaryName());\n}\n\n// How many worker processes, when nobody said.\n//\n// Half the cores, capped. The cap is there because a worker is a process with\n// blink in it, not a thread: measured on this tree it settles around 14 MB of\n// private working set and holds a further ~30 MB of shotium.exe resident, so\n// \"half the cores\" on a 32-core machine is sixteen of them and most of a\n// gigabyte for a queue that is almost never sixteen deep. Four is past the\n// point where a screenshot workload gets much from another one -- the corpus\n// runs at 41 pages/s on four -- and anyone who has measured otherwise passes\n// `workers`.\nconst MAXIMUM_DEFAULT_WORKERS = 4;\n\nfunction defaultWorkers(): number {\n const half = Math.floor((os.cpus().length || 2) / 2);\n return Math.max(1, Math.min(MAXIMUM_DEFAULT_WORKERS, half));\n}\n\nfunction defaultCacheDir(): string {\n return path.join(os.tmpdir(), 'shotium-cache');\n}\n\n// binary / workers / cacheDir / args, filled in and normalised. `cacheDir:\n// null` survives as null -- it means \"no disk cache\", which is not the same\n// request as \"use the default one\".\nfunction resolveStartOptions(options: StartOptions = {}): ResolvedStartOptions {\n return {\n binary: options.binary || defaultBinary(),\n workers: options.workers || defaultWorkers(),\n cacheDir: options.cacheDir === null ?\n null :\n (options.cacheDir || defaultCacheDir()),\n args: options.args || [],\n };\n}\n\nexport {\n defaultBinary,\n defaultCacheDir,\n defaultWorkers,\n resolveStartOptions,\n};\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 binary?: string;\n workers?: number;\n cacheDir?: string|null;\n args?: 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 -- binary, worker count, cache\n// root, extra flags -- rather than a fixed name, because attaching to whatever\n// daemon happens to be up would mean rendering with someone else's binary and\n// someone else's flags. Two configurations are two daemons; the same\n// configuration, from any process, is one.\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 path.resolve(options.binary || ''),\n options.workers,\n options.cacheDir === null ? null : path.resolve(options.cacheDir || ''),\n options.args || [],\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 {spawn} from 'node:child_process';\nimport type {ChildProcess, StdioOptions} from 'node:child_process';\nimport {EventEmitter} from 'node:events';\n\nimport {FrameReader, encodeRequest} from './protocol.js';\nimport type {WireRequest} from './request.js';\n\nexport interface WorkerOptions {\n id: number;\n binary: string;\n args?: string[];\n}\n\n// The header frame the worker answers with, followed by the image frame.\nexport interface ResponseHeader {\n ok: boolean;\n error?: string;\n bytes?: number;\n path?: string;\n}\n\nexport interface WorkerResult {\n header: ResponseHeader;\n image: Buffer|null;\n}\n\ninterface Pending {\n resolve: (result: WorkerResult) => void;\n reject: (error: Error) => void;\n}\n\n// One shotium.exe --serve process.\n//\n// Exactly one request is in flight at a time, and that is not a simplification:\n// blink is a process-wide singleton bound to the worker's main thread, so a\n// second request could not be rendered concurrently even if the protocol\n// allowed it. Concurrency is the pool's job, and it gets it by running more\n// processes.\n//\n// Events:\n// ready the process has started\n// exit {code, signal} it is gone, for any reason\n// crash {code, signal} it is gone while it owed an answer\n// stderr {line} a diagnostic line, useful when a render is wrong\nclass Worker extends EventEmitter {\n readonly id: number;\n // How many requests this process has answered, either way. The pool reads\n // it to tell a worker that was working and then died from one that never\n // came up at all.\n served = 0;\n\n private readonly binary: string;\n private readonly args: string[];\n private process: ChildProcess|null = null;\n private pending: Pending|null = null;\n private stopping = false;\n private reader = new FrameReader();\n private header: ResponseHeader|null = null;\n private stderr = '';\n\n constructor(options: WorkerOptions) {\n super();\n this.id = options.id;\n this.binary = options.binary;\n this.args = options.args || [];\n this.start();\n }\n\n get busy(): boolean {\n return this.pending !== null;\n }\n\n get alive(): boolean {\n return this.process !== null && this.process.exitCode === null &&\n !this.stopping;\n }\n\n private start(): void {\n const stdio: StdioOptions = ['pipe', 'pipe', 'pipe'];\n const child = spawn(this.binary, ['--serve', ...this.args], {\n stdio,\n windowsHide: true,\n // Detached, which on Windows means DETACHED_PROCESS: no console, and so\n // no conhost.exe beside every worker. Four of those cost 40 MB of\n // working set for a console nothing writes to -- the worker's output is\n // three pipes.\n //\n // It does not outlive its supervisor despite the name: the worker exits\n // when its stdin closes, and stdin closes when this process dies.\n detached: true,\n });\n this.process = child;\n\n child.stdout?.on('data', (chunk: Buffer) => this.onStdout(chunk));\n child.stderr?.on('data', (chunk: Buffer) => this.onStderr(chunk));\n child.on('error', (error) => this.onGone(null, null, error));\n child.on('exit', (code, signal) => this.onGone(code, signal, null));\n\n // The process is up as soon as spawn resolves the executable; there is no\n // handshake in the protocol, and adding one would only move the failure --\n // a binary that cannot start fails the first request just as visibly.\n this.emit('ready');\n }\n\n private onStdout(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 // Frame one of two: the JSON header.\n try {\n this.header = JSON.parse(frame.toString('utf8')) as ResponseHeader;\n } catch {\n this.fail(new Error(\n `shotium: worker ${this.id} sent a header that is not JSON`));\n return;\n }\n continue;\n }\n // Frame two: the image, empty when the header reported a failure or when\n // the worker was asked to write the file itself.\n const header = this.header;\n this.header = null;\n this.settle(header, frame);\n }\n }\n\n private onStderr(chunk: Buffer): void {\n this.stderr += chunk.toString('utf8');\n const lines = this.stderr.split(/\\r?\\n/);\n this.stderr = lines.pop() ?? '';\n for (const line of lines) {\n if (line.length > 0) {\n this.emit('stderr', {worker: this.id, line});\n }\n }\n }\n\n private onGone(\n code: number|null, signal: NodeJS.Signals|null,\n error: Error|null): void {\n const wasOwed = this.pending !== null;\n this.process = null;\n if (wasOwed) {\n // A worker that dies mid-request is indistinguishable from one that never\n // answered, which is the point: the retry path does not have to tell a\n // crash from a hang.\n this.fail(\n error ||\n new Error(`shotium: worker ${this.id} exited (code ${code}, signal ${\n signal}) with a request in flight`));\n this.emit('crash', {worker: this.id, code, signal});\n } else if (error) {\n this.emit('error', error);\n }\n this.emit('exit', {worker: this.id, code, signal});\n }\n\n private settle(header: ResponseHeader, payload: Buffer): void {\n const pending = this.pending;\n if (!pending) {\n return;\n }\n this.pending = null;\n this.served += 1;\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 fail(error: Error): void {\n const pending = this.pending;\n if (!pending) {\n return;\n }\n this.pending = null;\n pending.reject(error);\n }\n\n // Sends one request. Rejects if the worker dies before answering; there is no\n // timeout here, because the deadline belongs to whoever owns the retry\n // policy.\n send(request: WireRequest): Promise<WorkerResult> {\n if (this.pending) {\n return Promise.reject(\n new Error(`shotium: worker ${this.id} is already busy`));\n }\n if (!this.alive) {\n return Promise.reject(new Error(`shotium: worker ${this.id} is not up`));\n }\n return new Promise<WorkerResult>((resolve, reject) => {\n this.pending = {resolve, reject};\n this.process?.stdin?.write(encodeRequest(request), (error) => {\n if (error) {\n this.fail(error);\n }\n });\n });\n }\n\n // Closing stdin is the shutdown message: the worker sees the frame stream end\n // on a frame boundary and exits 0. kill() is for when it has stopped\n // listening.\n stop(): void {\n this.stopping = true;\n this.process?.stdin?.end();\n }\n\n kill(): void {\n this.stopping = true;\n this.process?.kill();\n }\n}\n\nexport {Worker};\n","import {EventEmitter} from 'node:events';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport type {ResolvedStartOptions} from './config.js';\nimport {defaultCacheDir} from './config.js';\nimport type {WireRequest} from './request.js';\nimport type {WorkerResult} from './worker.js';\nimport {Worker} from './worker.js';\n\n// A worker that exits sooner than this never really started, so its slot is\n// refilled on a doubling delay rather than immediately.\nconst FAST_FAILURE_MS = 1000;\nconst RESPAWN_DELAY_MS = 100;\nconst MAX_RESPAWN_DELAY_MS = 5000;\n\nexport interface SubmitOptions {\n /** The supervisor's deadline, in milliseconds. */\n timeout: number;\n /** How many times to re-send after a crash or a timeout. */\n retry: number;\n}\n\ninterface Job {\n request: WireRequest;\n timeout: number;\n attemptsLeft: number;\n resolve: (result: WorkerResult) => void;\n reject: (error: Error) => void;\n}\n\n// A fixed set of shotium.exe --serve processes, and a queue in front of them.\n//\n// The pool exists because blink is a process-wide singleton: one worker renders\n// one document at a time, so N concurrent screenshots means N processes. It is\n// also what makes a crash survivable -- a worker that dies takes its own\n// request down and nothing else, and the slot is refilled.\n//\n// Events:\n// ready {workers} the pool is up\n// exit {worker, code, signal} a worker is gone\n// crash {worker, code, signal} a worker died owing an answer\n// timeout {worker, timeout} a request outlived its deadline\n// worker-restart {worker, reason, delay} a slot was refilled\n// worker-error {worker, error} a worker could not be started\n// stderr {worker, line} a diagnostic line from a worker\nclass Pool extends EventEmitter {\n private readonly binary: string;\n private readonly size: number;\n private readonly args: string[];\n private readonly cacheDir: string|null;\n private slots: Worker[] = [];\n private failures: number[] = [];\n private queue: Job[] = [];\n private stopping = false;\n private nextId = 0;\n\n constructor(options: ResolvedStartOptions) {\n super();\n this.binary = options.binary;\n this.size = options.workers;\n this.args = options.args || [];\n this.cacheDir = options.cacheDir || null;\n }\n\n start(): void {\n if (this.slots.length > 0) {\n return;\n }\n for (let slot = 0; slot < this.size; ++slot) {\n this.slots[slot] = this.spawn(slot);\n }\n this.emit('ready', {workers: this.size});\n }\n\n private spawn(slot: number): Worker {\n const id = this.nextId++;\n const args = [...this.args];\n if (this.cacheDir) {\n // One directory per slot, not per process: the Simple backend takes an\n // exclusive lock on its directory, so sharing one would leave every\n // worker but the first running uncached. Keying on the slot rather than\n // the worker id means a restarted worker inherits the warm cache its\n // predecessor built.\n const dir = path.join(this.cacheDir, `worker-${slot}`);\n fs.mkdirSync(dir, {recursive: true});\n args.push(`--cache-dir=${dir}`);\n }\n\n const startedAt = Date.now();\n const worker = new Worker({id, binary: this.binary, args});\n worker.on('stderr', (event) => this.emit('stderr', event));\n worker.on('crash', (event) => this.emit('crash', event));\n // A worker that could not be started at all -- a binary that is not there,\n // a path that is not executable -- reports it here. Without a listener\n // EventEmitter throws the error instead, which for a resident daemon means\n // a typo in a path takes the whole pool down.\n worker.on('error', (error) => this.emit('worker-error', {worker: id, error}));\n worker.on('exit', (event) => {\n this.emit('exit', event);\n if (this.stopping || this.slots[slot] !== worker) {\n return;\n }\n // A worker that died on the way up is not a crash to recover from, it is\n // a configuration that does not work, and refilling the slot as fast as\n // the loop allows would spin a core until someone noticed. Back off, but\n // never give up: the binary may yet appear, and a pool that stopped\n // trying would have to be restarted by hand.\n //\n // \"On the way up\" is answered nothing and did not last a second, in that\n // order. Age alone would misread the ordinary case this design exists\n // for -- a worker killed mid-request seconds after the pool started --\n // as a startup failure, and delay the slot that the retry needs.\n const started = worker.served > 0 ||\n (Date.now() - startedAt) >= FAST_FAILURE_MS;\n if (started) {\n this.failures[slot] = 0;\n }\n const failures = this.failures[slot] || 0;\n const delay = started ?\n 0 :\n Math.min(MAX_RESPAWN_DELAY_MS, RESPAWN_DELAY_MS * 2 ** failures);\n this.failures[slot] = failures + 1;\n const refill = () => {\n if (this.stopping || this.slots[slot] !== worker) {\n return;\n }\n const replacement = this.spawn(slot);\n this.slots[slot] = replacement;\n this.emit('worker-restart',\n {worker: replacement.id, reason: 'exit', delay});\n this.pump();\n };\n if (delay === 0) {\n refill();\n return;\n }\n const timer = setTimeout(refill, delay);\n // An unref'd timer does not hold the process open: a pool whose workers\n // all failed should not be the reason a program refuses to exit.\n timer.unref();\n });\n return worker;\n }\n\n // Queues one request. `timeout` is the supervisor's deadline, which is longer\n // than the worker's own: the worker fails a slow page by itself and answers,\n // and this only fires when it has stopped answering at all.\n submit(request: WireRequest, {timeout, retry}: SubmitOptions):\n Promise<WorkerResult> {\n return new Promise<WorkerResult>((resolve, reject) => {\n this.queue.push({\n request,\n timeout,\n attemptsLeft: Math.max(0, retry) + 1,\n resolve,\n reject,\n });\n this.pump();\n });\n }\n\n private pump(): void {\n while (this.queue.length > 0) {\n const slot = this.slots.findIndex((w) => w && w.alive && !w.busy);\n if (slot < 0) {\n return;\n }\n this.dispatch(this.slots[slot]!, this.queue.shift()!);\n }\n }\n\n private dispatch(worker: Worker, job: Job): void {\n job.attemptsLeft -= 1;\n\n let settled = false;\n const timer = setTimeout(() => {\n if (settled) {\n return;\n }\n settled = true;\n this.emit('timeout', {worker: worker.id, timeout: job.timeout});\n // The worker is not answering, so the only way to get the slot back is to\n // take the process down. The exit handler refills the slot.\n worker.kill();\n this.retryOrFail(\n job,\n new Error(`shotium: no answer within ${job.timeout}ms`));\n }, job.timeout);\n\n worker.send(job.request)\n .then((result) => {\n if (settled) {\n return;\n }\n settled = true;\n clearTimeout(timer);\n job.resolve(result);\n this.pump();\n })\n .catch((error: Error) => {\n if (settled) {\n return;\n }\n settled = true;\n clearTimeout(timer);\n this.retryOrFail(job, error);\n });\n }\n\n private retryOrFail(job: Job, error: Error): void {\n // A request rejected on its own merits -- a bad selector, an unreadable\n // file -- would fail the same way every time, but the worker also rejects\n // with the same shape when it dies. Retrying both is the safe direction:\n // the cost of a pointless retry is one more render, and the cost of not\n // retrying a crash is a failure the caller cannot do anything about.\n if (job.attemptsLeft > 0 && !this.stopping) {\n this.queue.unshift(job);\n this.pump();\n return;\n }\n job.reject(error);\n this.pump();\n }\n\n async stop(): Promise<void> {\n this.stopping = true;\n for (const job of this.queue.splice(0)) {\n job.reject(new Error('shotium: the runtime was stopped'));\n }\n await Promise.all(this.slots.map((worker) => new Promise<void>((resolve) => {\n if (!worker || !worker.alive) {\n resolve();\n return;\n }\n worker.once('exit', () => resolve());\n worker.stop();\n })));\n this.slots = [];\n }\n}\n\nexport {Pool, defaultCacheDir};\n"],"mappings":"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"}
|
package/dist/request-qZXS3N9f.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
//#region src/lib/request.ts
|
|
2
|
-
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
3
|
-
const SUPERVISOR_MARGIN_MS = 1e4;
|
|
4
|
-
const WIRE_FIELDS = /* @__PURE__ */ new Set([
|
|
5
|
-
"file",
|
|
6
|
-
"type",
|
|
7
|
-
"fullPage",
|
|
8
|
-
"selector",
|
|
9
|
-
"quality",
|
|
10
|
-
"scale",
|
|
11
|
-
"omitBackground",
|
|
12
|
-
"path",
|
|
13
|
-
"pageGotoParams",
|
|
14
|
-
"clip",
|
|
15
|
-
"viewport",
|
|
16
|
-
"allowFileAccess"
|
|
17
|
-
]);
|
|
18
|
-
function toRequest(options) {
|
|
19
|
-
if (!options || typeof options !== "object") throw new TypeError("shotium: screenshot(options) needs an object");
|
|
20
|
-
if (typeof options.file !== "string" || options.file.length === 0) throw new TypeError("shotium: options.file is required");
|
|
21
|
-
const request = {};
|
|
22
|
-
for (const [key, value] of Object.entries(options)) {
|
|
23
|
-
if (value === void 0) continue;
|
|
24
|
-
if (key === "retry") continue;
|
|
25
|
-
if (!WIRE_FIELDS.has(key)) throw new TypeError(`shotium: unknown option "${key}"`);
|
|
26
|
-
request[key] = value;
|
|
27
|
-
}
|
|
28
|
-
if (request.viewport) {
|
|
29
|
-
const { width, height } = request.viewport;
|
|
30
|
-
delete request.viewport;
|
|
31
|
-
if (width !== void 0) request.width = width;
|
|
32
|
-
if (height !== void 0) request.height = height;
|
|
33
|
-
}
|
|
34
|
-
return request;
|
|
35
|
-
}
|
|
36
|
-
function timeoutFor(options) {
|
|
37
|
-
const timeout = options.pageGotoParams && options.pageGotoParams.timeout;
|
|
38
|
-
return typeof timeout === "number" ? timeout : DEFAULT_TIMEOUT_MS;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
//#endregion
|
|
42
|
-
export { timeoutFor as n, toRequest as r, SUPERVISOR_MARGIN_MS as t };
|
|
43
|
-
//# sourceMappingURL=request-qZXS3N9f.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"request-qZXS3N9f.js","names":[],"sources":["../src/lib/request.ts"],"sourcesContent":["import type {Clip, PageGotoParams, ScreenshotOptions} from '../types.js';\n\nconst DEFAULT_TIMEOUT_MS = 30000;\n// How much longer than the page's own deadline a supervisor waits before\n// deciding the worker is not going to answer at all. The worker fails a slow\n// page by itself and replies; this margin covers process startup and the\n// encode, and firing it means something worse than a slow page.\nconst SUPERVISOR_MARGIN_MS = 10000;\n\n// What actually goes down the pipe. It is ScreenshotOptions with the viewport\n// flattened and `retry` taken out -- see toRequest below for why each.\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 in-process pool and the\n// 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 // retry is the supervisor's, not the worker's: it decides how many times a\n // request is re-sent, which is not something the worker could act on.\n if (key === 'retry') {\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 SUPERVISOR_MARGIN_MS,\n WIRE_FIELDS,\n timeoutFor,\n toRequest,\n};\n"],"mappings":";AAEA,MAAM,qBAAqB;AAK3B,MAAM,uBAAuB;AA2B7B,MAAM,8BAAc,IAAI,IAAI;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAOD,SAAS,UAAU,SAAyC;CAC1D,IAAI,CAAC,WAAW,OAAO,YAAY,UACjC,MAAM,IAAI,UAAU,8CAA8C;CAEpE,IAAI,OAAO,QAAQ,SAAS,YAAY,QAAQ,KAAK,WAAW,GAC9D,MAAM,IAAI,UAAU,mCAAmC;CAGzD,MAAM,UAAmC,CAAC;CAC1C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,GAAG;EAClD,IAAI,UAAU,QACZ;EAIF,IAAI,QAAQ,SACV;EAEF,IAAI,CAAC,YAAY,IAAI,GAAG,GACtB,MAAM,IAAI,UAAU,4BAA4B,IAAI,EAAE;EAExD,QAAQ,OAAO;CACjB;CAIA,IAAI,QAAQ,UAAU;EACpB,MAAM,EAAC,OAAO,WAAU,QAAQ;EAIhC,OAAO,QAAQ;EACf,IAAI,UAAU,QACZ,QAAQ,QAAQ;EAElB,IAAI,WAAW,QACb,QAAQ,SAAS;CAErB;CACA,OAAO;AACT;AAEA,SAAS,WAAW,SAAoC;CACtD,MAAM,UAAU,QAAQ,kBAAkB,QAAQ,eAAe;CACjE,OAAO,OAAO,YAAY,WAAW,UAAU;AACjD"}
|
package/dist/types-x9HtkzeE.d.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
//#region src/types.d.ts
|
|
2
|
-
/** A region of the document, in CSS pixels. */
|
|
3
|
-
interface Clip {
|
|
4
|
-
x: number;
|
|
5
|
-
y: number;
|
|
6
|
-
width: number;
|
|
7
|
-
height: number;
|
|
8
|
-
}
|
|
9
|
-
interface PageGotoParams {
|
|
10
|
-
/** Milliseconds before the load is abandoned. Default 30000. */
|
|
11
|
-
timeout?: number;
|
|
12
|
-
/**
|
|
13
|
-
* `load` waits for parsing to finish, the load event to fire and every
|
|
14
|
-
* request to complete. `networkidle` additionally waits for a 500ms window
|
|
15
|
-
* with nothing in flight, which matters for documents that keep fetching
|
|
16
|
-
* after the load event -- CSS that pulls in more CSS, or a font a late style
|
|
17
|
-
* change brought in.
|
|
18
|
-
*/
|
|
19
|
-
waitUntil?: 'load' | 'networkidle';
|
|
20
|
-
}
|
|
21
|
-
/** The viewport the document is laid out in. */
|
|
22
|
-
interface Viewport {
|
|
23
|
-
/** CSS pixels. Default 1280. */
|
|
24
|
-
width?: number;
|
|
25
|
-
/** CSS pixels. Default 720. */
|
|
26
|
-
height?: number;
|
|
27
|
-
}
|
|
28
|
-
interface ScreenshotOptions {
|
|
29
|
-
/** An http/https/file URL, or a local path. */
|
|
30
|
-
file: string;
|
|
31
|
-
/** Default `png`. */
|
|
32
|
-
type?: 'png' | 'jpeg' | 'webp';
|
|
33
|
-
/** Capture the whole document rather than the viewport. */
|
|
34
|
-
fullPage?: boolean;
|
|
35
|
-
/**
|
|
36
|
-
* Capture the box of the first element matching this CSS selector. Resolved
|
|
37
|
-
* inside the renderer with Document::querySelector -- there is no JavaScript
|
|
38
|
-
* engine, so nothing is injected into the page.
|
|
39
|
-
*/
|
|
40
|
-
selector?: string;
|
|
41
|
-
/** 1-100, `jpeg` and `webp` only. Default 90. */
|
|
42
|
-
quality?: number;
|
|
43
|
-
/** Device scale factor, 0.01-8. Default 1. */
|
|
44
|
-
scale?: number;
|
|
45
|
-
/**
|
|
46
|
-
* Keep the alpha channel instead of painting the page's white backdrop.
|
|
47
|
-
* Rejected for `jpeg`, which has no alpha channel.
|
|
48
|
-
*/
|
|
49
|
-
omitBackground?: boolean;
|
|
50
|
-
/** Write the image here instead of returning it, saving a round trip. */
|
|
51
|
-
path?: string;
|
|
52
|
-
pageGotoParams?: PageGotoParams;
|
|
53
|
-
/** A region of the document, in CSS pixels. */
|
|
54
|
-
clip?: Clip;
|
|
55
|
-
/** The viewport the document is laid out in. */
|
|
56
|
-
viewport?: Viewport;
|
|
57
|
-
/**
|
|
58
|
-
* Let the document read `file:` subresources. Off by default: a library does
|
|
59
|
-
* not get to decide for its caller that a document may read the filesystem it
|
|
60
|
-
* is rendered on.
|
|
61
|
-
*/
|
|
62
|
-
allowFileAccess?: boolean;
|
|
63
|
-
/** How many times to re-send after a crash or a timeout. Default 0. */
|
|
64
|
-
retry?: number;
|
|
65
|
-
}
|
|
66
|
-
interface StartOptions {
|
|
67
|
-
/**
|
|
68
|
-
* Path to `shotium.exe`. Default `$SHOTIUM_BINARY`, then the platform
|
|
69
|
-
* package for this machine, then `./bin/shotium.exe`.
|
|
70
|
-
*/
|
|
71
|
-
binary?: string;
|
|
72
|
-
/** Worker processes. Default half the cores, at least one, at most four. */
|
|
73
|
-
workers?: number;
|
|
74
|
-
/** Root of the per-worker HTTP disk caches. `null` disables caching. */
|
|
75
|
-
cacheDir?: string | null;
|
|
76
|
-
/** Extra flags passed to every worker. */
|
|
77
|
-
args?: string[];
|
|
78
|
-
}
|
|
79
|
-
interface WorkerEvent {
|
|
80
|
-
worker: number;
|
|
81
|
-
code?: number | null;
|
|
82
|
-
signal?: NodeJS.Signals | null;
|
|
83
|
-
}
|
|
84
|
-
interface DaemonOptions extends StartOptions {
|
|
85
|
-
/**
|
|
86
|
-
* Address the daemon by name instead of by configuration. Without it the
|
|
87
|
-
* endpoint is a hash of `binary`, `workers`, `cacheDir` and `args`, so a
|
|
88
|
-
* client never attaches to a pool that renders with something other than
|
|
89
|
-
* what it asked for.
|
|
90
|
-
*/
|
|
91
|
-
name?: string;
|
|
92
|
-
/** The pipe or socket to use, overriding both the name and the hash. */
|
|
93
|
-
endpoint?: string;
|
|
94
|
-
/**
|
|
95
|
-
* Exit after this long with no connections and nothing rendering. Default
|
|
96
|
-
* 300000; `0` never exits.
|
|
97
|
-
*/
|
|
98
|
-
idleTimeoutMs?: number;
|
|
99
|
-
/**
|
|
100
|
-
* Render one throwaway document per worker at startup, so the first real
|
|
101
|
-
* request does not pay for whatever a worker initialises lazily. Default
|
|
102
|
-
* true.
|
|
103
|
-
*/
|
|
104
|
-
prewarm?: boolean;
|
|
105
|
-
/** Fail instead of starting a daemon when none is listening. */
|
|
106
|
-
spawn?: boolean;
|
|
107
|
-
/** Where a spawned daemon's diagnostics go. Default `$SHOTIUM_DAEMON_LOG`. */
|
|
108
|
-
logFile?: string;
|
|
109
|
-
/** How long to wait for a daemon this process started to bind. */
|
|
110
|
-
startTimeoutMs?: number;
|
|
111
|
-
}
|
|
112
|
-
interface DaemonStatus {
|
|
113
|
-
ok?: boolean;
|
|
114
|
-
running?: boolean;
|
|
115
|
-
spawned?: boolean;
|
|
116
|
-
pid: number;
|
|
117
|
-
endpoint: string;
|
|
118
|
-
binary: string;
|
|
119
|
-
workers: number;
|
|
120
|
-
cacheDir: string | null;
|
|
121
|
-
args: string[];
|
|
122
|
-
/** Every worker has rendered at least once. */
|
|
123
|
-
warm: boolean;
|
|
124
|
-
uptimeMs: number;
|
|
125
|
-
connections: number;
|
|
126
|
-
inFlight: number;
|
|
127
|
-
served: number;
|
|
128
|
-
idleTimeoutMs: number;
|
|
129
|
-
version: string;
|
|
130
|
-
}
|
|
131
|
-
interface NativeStartOptions {
|
|
132
|
-
/**
|
|
133
|
-
* Root of the HTTP disk cache. `null` disables caching entirely, which is
|
|
134
|
-
* the default here: an in-process engine is often a short-lived program, and
|
|
135
|
-
* a cache it never reads twice is a directory it leaves behind.
|
|
136
|
-
*/
|
|
137
|
-
cacheDir?: string | null;
|
|
138
|
-
/** Overrides the built-in user agent string. */
|
|
139
|
-
userAgent?: string;
|
|
140
|
-
/**
|
|
141
|
-
* Where `shotium_data.pak` and `shotium_strings.pak` are. Defaults to the
|
|
142
|
-
* directory the native engine was loaded from, which is where they ship.
|
|
143
|
-
*/
|
|
144
|
-
resourceDir?: string;
|
|
145
|
-
}
|
|
146
|
-
interface PurgeOptions {
|
|
147
|
-
/**
|
|
148
|
-
* Also ask the OS to take the engine's pages back. The next screenshot pays
|
|
149
|
-
* them back in soft page faults -- a few milliseconds -- so this is for when
|
|
150
|
-
* there may not be a next one soon.
|
|
151
|
-
*/
|
|
152
|
-
releaseWorkingSet?: boolean;
|
|
153
|
-
}
|
|
154
|
-
//#endregion
|
|
155
|
-
export { PageGotoParams as a, StartOptions as c, NativeStartOptions as i, Viewport as l, DaemonOptions as n, PurgeOptions as o, DaemonStatus as r, ScreenshotOptions as s, Clip as t, WorkerEvent as u };
|
|
156
|
-
//# sourceMappingURL=types-x9HtkzeE.d.ts.map
|
package/src/lib/pool.ts
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import {EventEmitter} from 'node:events';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
import type {ResolvedStartOptions} from './config.js';
|
|
6
|
-
import {defaultCacheDir} from './config.js';
|
|
7
|
-
import type {WireRequest} from './request.js';
|
|
8
|
-
import type {WorkerResult} from './worker.js';
|
|
9
|
-
import {Worker} from './worker.js';
|
|
10
|
-
|
|
11
|
-
// A worker that exits sooner than this never really started, so its slot is
|
|
12
|
-
// refilled on a doubling delay rather than immediately.
|
|
13
|
-
const FAST_FAILURE_MS = 1000;
|
|
14
|
-
const RESPAWN_DELAY_MS = 100;
|
|
15
|
-
const MAX_RESPAWN_DELAY_MS = 5000;
|
|
16
|
-
|
|
17
|
-
export interface SubmitOptions {
|
|
18
|
-
/** The supervisor's deadline, in milliseconds. */
|
|
19
|
-
timeout: number;
|
|
20
|
-
/** How many times to re-send after a crash or a timeout. */
|
|
21
|
-
retry: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface Job {
|
|
25
|
-
request: WireRequest;
|
|
26
|
-
timeout: number;
|
|
27
|
-
attemptsLeft: number;
|
|
28
|
-
resolve: (result: WorkerResult) => void;
|
|
29
|
-
reject: (error: Error) => void;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// A fixed set of shotium.exe --serve processes, and a queue in front of them.
|
|
33
|
-
//
|
|
34
|
-
// The pool exists because blink is a process-wide singleton: one worker renders
|
|
35
|
-
// one document at a time, so N concurrent screenshots means N processes. It is
|
|
36
|
-
// also what makes a crash survivable -- a worker that dies takes its own
|
|
37
|
-
// request down and nothing else, and the slot is refilled.
|
|
38
|
-
//
|
|
39
|
-
// Events:
|
|
40
|
-
// ready {workers} the pool is up
|
|
41
|
-
// exit {worker, code, signal} a worker is gone
|
|
42
|
-
// crash {worker, code, signal} a worker died owing an answer
|
|
43
|
-
// timeout {worker, timeout} a request outlived its deadline
|
|
44
|
-
// worker-restart {worker, reason, delay} a slot was refilled
|
|
45
|
-
// worker-error {worker, error} a worker could not be started
|
|
46
|
-
// stderr {worker, line} a diagnostic line from a worker
|
|
47
|
-
class Pool extends EventEmitter {
|
|
48
|
-
private readonly binary: string;
|
|
49
|
-
private readonly size: number;
|
|
50
|
-
private readonly args: string[];
|
|
51
|
-
private readonly cacheDir: string|null;
|
|
52
|
-
private slots: Worker[] = [];
|
|
53
|
-
private failures: number[] = [];
|
|
54
|
-
private queue: Job[] = [];
|
|
55
|
-
private stopping = false;
|
|
56
|
-
private nextId = 0;
|
|
57
|
-
|
|
58
|
-
constructor(options: ResolvedStartOptions) {
|
|
59
|
-
super();
|
|
60
|
-
this.binary = options.binary;
|
|
61
|
-
this.size = options.workers;
|
|
62
|
-
this.args = options.args || [];
|
|
63
|
-
this.cacheDir = options.cacheDir || null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
start(): void {
|
|
67
|
-
if (this.slots.length > 0) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
for (let slot = 0; slot < this.size; ++slot) {
|
|
71
|
-
this.slots[slot] = this.spawn(slot);
|
|
72
|
-
}
|
|
73
|
-
this.emit('ready', {workers: this.size});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private spawn(slot: number): Worker {
|
|
77
|
-
const id = this.nextId++;
|
|
78
|
-
const args = [...this.args];
|
|
79
|
-
if (this.cacheDir) {
|
|
80
|
-
// One directory per slot, not per process: the Simple backend takes an
|
|
81
|
-
// exclusive lock on its directory, so sharing one would leave every
|
|
82
|
-
// worker but the first running uncached. Keying on the slot rather than
|
|
83
|
-
// the worker id means a restarted worker inherits the warm cache its
|
|
84
|
-
// predecessor built.
|
|
85
|
-
const dir = path.join(this.cacheDir, `worker-${slot}`);
|
|
86
|
-
fs.mkdirSync(dir, {recursive: true});
|
|
87
|
-
args.push(`--cache-dir=${dir}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const startedAt = Date.now();
|
|
91
|
-
const worker = new Worker({id, binary: this.binary, args});
|
|
92
|
-
worker.on('stderr', (event) => this.emit('stderr', event));
|
|
93
|
-
worker.on('crash', (event) => this.emit('crash', event));
|
|
94
|
-
// A worker that could not be started at all -- a binary that is not there,
|
|
95
|
-
// a path that is not executable -- reports it here. Without a listener
|
|
96
|
-
// EventEmitter throws the error instead, which for a resident daemon means
|
|
97
|
-
// a typo in a path takes the whole pool down.
|
|
98
|
-
worker.on('error', (error) => this.emit('worker-error', {worker: id, error}));
|
|
99
|
-
worker.on('exit', (event) => {
|
|
100
|
-
this.emit('exit', event);
|
|
101
|
-
if (this.stopping || this.slots[slot] !== worker) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
// A worker that died on the way up is not a crash to recover from, it is
|
|
105
|
-
// a configuration that does not work, and refilling the slot as fast as
|
|
106
|
-
// the loop allows would spin a core until someone noticed. Back off, but
|
|
107
|
-
// never give up: the binary may yet appear, and a pool that stopped
|
|
108
|
-
// trying would have to be restarted by hand.
|
|
109
|
-
//
|
|
110
|
-
// "On the way up" is answered nothing and did not last a second, in that
|
|
111
|
-
// order. Age alone would misread the ordinary case this design exists
|
|
112
|
-
// for -- a worker killed mid-request seconds after the pool started --
|
|
113
|
-
// as a startup failure, and delay the slot that the retry needs.
|
|
114
|
-
const started = worker.served > 0 ||
|
|
115
|
-
(Date.now() - startedAt) >= FAST_FAILURE_MS;
|
|
116
|
-
if (started) {
|
|
117
|
-
this.failures[slot] = 0;
|
|
118
|
-
}
|
|
119
|
-
const failures = this.failures[slot] || 0;
|
|
120
|
-
const delay = started ?
|
|
121
|
-
0 :
|
|
122
|
-
Math.min(MAX_RESPAWN_DELAY_MS, RESPAWN_DELAY_MS * 2 ** failures);
|
|
123
|
-
this.failures[slot] = failures + 1;
|
|
124
|
-
const refill = () => {
|
|
125
|
-
if (this.stopping || this.slots[slot] !== worker) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const replacement = this.spawn(slot);
|
|
129
|
-
this.slots[slot] = replacement;
|
|
130
|
-
this.emit('worker-restart',
|
|
131
|
-
{worker: replacement.id, reason: 'exit', delay});
|
|
132
|
-
this.pump();
|
|
133
|
-
};
|
|
134
|
-
if (delay === 0) {
|
|
135
|
-
refill();
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
const timer = setTimeout(refill, delay);
|
|
139
|
-
// An unref'd timer does not hold the process open: a pool whose workers
|
|
140
|
-
// all failed should not be the reason a program refuses to exit.
|
|
141
|
-
timer.unref();
|
|
142
|
-
});
|
|
143
|
-
return worker;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Queues one request. `timeout` is the supervisor's deadline, which is longer
|
|
147
|
-
// than the worker's own: the worker fails a slow page by itself and answers,
|
|
148
|
-
// and this only fires when it has stopped answering at all.
|
|
149
|
-
submit(request: WireRequest, {timeout, retry}: SubmitOptions):
|
|
150
|
-
Promise<WorkerResult> {
|
|
151
|
-
return new Promise<WorkerResult>((resolve, reject) => {
|
|
152
|
-
this.queue.push({
|
|
153
|
-
request,
|
|
154
|
-
timeout,
|
|
155
|
-
attemptsLeft: Math.max(0, retry) + 1,
|
|
156
|
-
resolve,
|
|
157
|
-
reject,
|
|
158
|
-
});
|
|
159
|
-
this.pump();
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
private pump(): void {
|
|
164
|
-
while (this.queue.length > 0) {
|
|
165
|
-
const slot = this.slots.findIndex((w) => w && w.alive && !w.busy);
|
|
166
|
-
if (slot < 0) {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
this.dispatch(this.slots[slot]!, this.queue.shift()!);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private dispatch(worker: Worker, job: Job): void {
|
|
174
|
-
job.attemptsLeft -= 1;
|
|
175
|
-
|
|
176
|
-
let settled = false;
|
|
177
|
-
const timer = setTimeout(() => {
|
|
178
|
-
if (settled) {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
settled = true;
|
|
182
|
-
this.emit('timeout', {worker: worker.id, timeout: job.timeout});
|
|
183
|
-
// The worker is not answering, so the only way to get the slot back is to
|
|
184
|
-
// take the process down. The exit handler refills the slot.
|
|
185
|
-
worker.kill();
|
|
186
|
-
this.retryOrFail(
|
|
187
|
-
job,
|
|
188
|
-
new Error(`shotium: no answer within ${job.timeout}ms`));
|
|
189
|
-
}, job.timeout);
|
|
190
|
-
|
|
191
|
-
worker.send(job.request)
|
|
192
|
-
.then((result) => {
|
|
193
|
-
if (settled) {
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
settled = true;
|
|
197
|
-
clearTimeout(timer);
|
|
198
|
-
job.resolve(result);
|
|
199
|
-
this.pump();
|
|
200
|
-
})
|
|
201
|
-
.catch((error: Error) => {
|
|
202
|
-
if (settled) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
settled = true;
|
|
206
|
-
clearTimeout(timer);
|
|
207
|
-
this.retryOrFail(job, error);
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
private retryOrFail(job: Job, error: Error): void {
|
|
212
|
-
// A request rejected on its own merits -- a bad selector, an unreadable
|
|
213
|
-
// file -- would fail the same way every time, but the worker also rejects
|
|
214
|
-
// with the same shape when it dies. Retrying both is the safe direction:
|
|
215
|
-
// the cost of a pointless retry is one more render, and the cost of not
|
|
216
|
-
// retrying a crash is a failure the caller cannot do anything about.
|
|
217
|
-
if (job.attemptsLeft > 0 && !this.stopping) {
|
|
218
|
-
this.queue.unshift(job);
|
|
219
|
-
this.pump();
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
job.reject(error);
|
|
223
|
-
this.pump();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async stop(): Promise<void> {
|
|
227
|
-
this.stopping = true;
|
|
228
|
-
for (const job of this.queue.splice(0)) {
|
|
229
|
-
job.reject(new Error('shotium: the runtime was stopped'));
|
|
230
|
-
}
|
|
231
|
-
await Promise.all(this.slots.map((worker) => new Promise<void>((resolve) => {
|
|
232
|
-
if (!worker || !worker.alive) {
|
|
233
|
-
resolve();
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
worker.once('exit', () => resolve());
|
|
237
|
-
worker.stop();
|
|
238
|
-
})));
|
|
239
|
-
this.slots = [];
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export {Pool, defaultCacheDir};
|