fullbleed 0.3.2 → 0.4.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 +42 -4
- package/dist/browser/asset-manifest.json +4 -4
- package/dist/browser/client.js +1 -1
- package/dist/browser/worker.js +2 -2
- package/dist/build.json +5 -5
- package/dist/engine.wasm +0 -0
- package/dist/index.d.cts +19 -1
- package/package.json +1 -1
- package/src/index.cjs +7 -0
- package/src/index.d.ts +19 -1
- package/src/index.js +1 -1
- package/src/render-queue.cjs +124 -0
package/README.md
CHANGED
|
@@ -15,9 +15,9 @@ Using Vue? The [Vue and TypeScript starter](examples/vue) includes editable
|
|
|
15
15
|
templates, automatic previews, downloads, and a reusable `usePdfPreview` composable.
|
|
16
16
|
|
|
17
17
|
This is an optional integration around the published **Fullbleed 2.5.11** engine.
|
|
18
|
-
The package has its own version, **0.
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
The package has its own version, **0.4.0**. Node applications can use a shared
|
|
19
|
+
render queue to bound active renders and waiting requests, include queue time in
|
|
20
|
+
deadlines, cancel waiting jobs, and stop rendering cleanly during shutdown.
|
|
21
21
|
The browser entry renders in a Web Worker, and the Node API supports worker and
|
|
22
22
|
process isolation. See the [browser guide](docs/browser.md),
|
|
23
23
|
[inline wrapping checks](docs/inline-wrapping.md),
|
|
@@ -33,7 +33,7 @@ Use Node.js 22 or newer. Install the [npm package](https://www.npmjs.com/package
|
|
|
33
33
|
npm install fullbleed
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
For a version-pinned installation, use `npm install --save-exact fullbleed@0.
|
|
36
|
+
For a version-pinned installation, use `npm install --save-exact fullbleed@0.4.0`.
|
|
37
37
|
Package archives and retained PDF/PNG evidence are attached to each
|
|
38
38
|
[GitHub release](https://github.com/fullbleed-engine/fullbleed-node/releases).
|
|
39
39
|
The [installation verification records](https://github.com/fullbleed-engine/fullbleed-node/tree/main/verification)
|
|
@@ -157,6 +157,36 @@ cover the twelve Latin faces, an embedded-font control, and the blank-preview
|
|
|
157
157
|
failure in public 0.3.1. Font provenance and complete license notices ship with
|
|
158
158
|
both the npm package and copied browser runtime.
|
|
159
159
|
|
|
160
|
+
## Limit concurrent renders
|
|
161
|
+
|
|
162
|
+
Create one queue per application process and reuse it across requests:
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
import { createRenderQueue } from 'fullbleed';
|
|
166
|
+
|
|
167
|
+
const documents = createRenderQueue({ concurrency: 2, maxQueue: 4 });
|
|
168
|
+
const result = await documents.renderPdf({
|
|
169
|
+
html: '<h1>Queued invoice</h1>',
|
|
170
|
+
isolation: 'process',
|
|
171
|
+
timeoutMs: 30_000,
|
|
172
|
+
});
|
|
173
|
+
// Return result.pdf from your request handler.
|
|
174
|
+
// At application shutdown, cancel outstanding work and await cleanup:
|
|
175
|
+
await documents.close();
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
This queue admits two active renders and four waiting requests. Further requests
|
|
179
|
+
reject with `QUEUE_FULL`, so a server can return a busy response. Waiting jobs
|
|
180
|
+
can be aborted and their deadlines include the wait. Slots stay occupied until
|
|
181
|
+
their worker or child process has exited. `close()` rejects new work, cancels
|
|
182
|
+
accepted jobs with `QUEUE_CLOSED`, and waits for cleanup.
|
|
183
|
+
|
|
184
|
+
Choose limits for your host and document sizes. The queue bounds job counts, not
|
|
185
|
+
total memory, and each application process has its own queue. It is an in-memory
|
|
186
|
+
admission control; use your application's durable job system for persistence and
|
|
187
|
+
delivery. See the [queue guide and retained checks](docs/render-queue.md) and the
|
|
188
|
+
[six-invoice batch example](examples/render-queue.mjs).
|
|
189
|
+
|
|
160
190
|
## API
|
|
161
191
|
|
|
162
192
|
This section describes the Node entry. The browser entry returns browser byte
|
|
@@ -225,6 +255,14 @@ Error codes include `INVALID_INPUT`, `MISSING_GLYPHS`, `PAGE_LIMIT`, `TIMEOUT`,
|
|
|
225
255
|
include `exitCode` and `signal`. Failed or cancelled calls do not return a partial
|
|
226
256
|
PDF. Caller-owned asset buffers are copied when a call starts.
|
|
227
257
|
|
|
258
|
+
`createRenderQueue({ concurrency?, maxQueue? })` exposes `renderPdf()` with the
|
|
259
|
+
same options and result. Defaults are one active render and sixteen waiting
|
|
260
|
+
jobs. `maxQueue: 0` rejects buffering. Accepted calls snapshot their strings and
|
|
261
|
+
asset/font bytes immediately. The queue adds `QUEUE_FULL` and `QUEUE_CLOSED`,
|
|
262
|
+
read-only `activeCount`, `pendingCount`, and `closed` properties, and an
|
|
263
|
+
idempotent asynchronous `close()` method. It does not reuse workers or change
|
|
264
|
+
the standalone `renderPdf()` concurrency behavior.
|
|
265
|
+
|
|
228
266
|
This API covers ordinary document generation and previews. It does not expose
|
|
229
267
|
PDF/A, PDF/UA, PDF/X, template overlays, or compiled VDP. Use the
|
|
230
268
|
[Python or Rust APIs](https://docs.fullbleed.dev/) for those workflows and their
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "fullbleed.browser-assets.v1",
|
|
3
|
-
"packageVersion": "0.
|
|
3
|
+
"packageVersion": "0.4.0",
|
|
4
4
|
"engineVersion": "2.5.11",
|
|
5
5
|
"files": {
|
|
6
6
|
"client.js": {
|
|
7
7
|
"source": "dist/browser/client.js",
|
|
8
8
|
"bytes": 12043,
|
|
9
|
-
"sha256": "
|
|
9
|
+
"sha256": "bf508fad7b4a3e2c542a54c088dce513f7b3356a8dd5e4be31cd3f6c73b63682"
|
|
10
10
|
},
|
|
11
11
|
"worker.js": {
|
|
12
12
|
"source": "dist/browser/worker.js",
|
|
13
13
|
"bytes": 52109,
|
|
14
|
-
"sha256": "
|
|
14
|
+
"sha256": "0fadb05bcb74518a119f78353072633791c2ce9a4e2bbd189e5b6d6e78de6353"
|
|
15
15
|
},
|
|
16
16
|
"engine.wasm": {
|
|
17
17
|
"source": "dist/engine.wasm",
|
|
18
18
|
"bytes": 5585689,
|
|
19
|
-
"sha256": "
|
|
19
|
+
"sha256": "cf8b2ad0493ee32a86871341805c1a5284ad6d2c14bb395593efa4a7c2db2cec"
|
|
20
20
|
},
|
|
21
21
|
"THIRD_PARTY_NOTICES.txt": {
|
|
22
22
|
"source": "dist/THIRD_PARTY_NOTICES.txt",
|
package/dist/browser/client.js
CHANGED
|
@@ -109,7 +109,7 @@ var require_render_input = __commonJS({
|
|
|
109
109
|
init_define_FULLBLEED_FONT_NAMES();
|
|
110
110
|
init_define_FULLBLEED_RUNTIME_FILES();
|
|
111
111
|
var import_render_input = __toESM(require_render_input(), 1);
|
|
112
|
-
var version = "0.
|
|
112
|
+
var version = "0.4.0";
|
|
113
113
|
var engineVersion = "2.5.11";
|
|
114
114
|
var FullbleedError = import_render_input.default.FullbleedError;
|
|
115
115
|
var invalid = (message) => {
|
package/dist/browser/worker.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
var define_FULLBLEED_FONT_NAMES_default = ["Inter-Variable.ttf", "DMSerifDisplay-Regular.ttf", "DMSerifDisplay-Italic.ttf", "BebasNeue-Regular.ttf"];
|
|
3
3
|
|
|
4
4
|
// <define:__FULLBLEED_RUNTIME_FILES__>
|
|
5
|
-
var define_FULLBLEED_RUNTIME_FILES_default = { "engine.wasm": { bytes: 5585689, sha256: "
|
|
5
|
+
var define_FULLBLEED_RUNTIME_FILES_default = { "engine.wasm": { bytes: 5585689, sha256: "cf8b2ad0493ee32a86871341805c1a5284ad6d2c14bb395593efa4a7c2db2cec" }, "fonts/Inter-Variable.ttf": { bytes: 876576, sha256: "29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031" }, "fonts/DMSerifDisplay-Regular.ttf": { bytes: 76580, sha256: "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616" }, "fonts/DMSerifDisplay-Italic.ttf": { bytes: 71208, sha256: "df74c0ac387baeaeb0fe4f2324e1668e6a3ed8c09cd9796fe162c71753e19e45" }, "fonts/BebasNeue-Regular.ttf": { bytes: 61400, sha256: "08e4623805102d819f58601e46e345648846075e363b2ceb23313c2d1c83ec73" } };
|
|
6
6
|
|
|
7
7
|
// node_modules/@bjorn3/browser_wasi_shim/dist/wasi_defs.js
|
|
8
8
|
var CLOCKID_REALTIME = 0;
|
|
@@ -1438,7 +1438,7 @@ async function runEngine(job) {
|
|
|
1438
1438
|
}
|
|
1439
1439
|
|
|
1440
1440
|
// src/browser-worker.js
|
|
1441
|
-
var version = "0.
|
|
1441
|
+
var version = "0.4.0";
|
|
1442
1442
|
var engineVersion = "2.5.11";
|
|
1443
1443
|
var files = define_FULLBLEED_RUNTIME_FILES_default;
|
|
1444
1444
|
var fontNames = define_FULLBLEED_FONT_NAMES_default;
|
package/dist/build.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"packageVersion": "0.
|
|
2
|
+
"packageVersion": "0.4.0",
|
|
3
3
|
"engineVersion": "2.5.11",
|
|
4
4
|
"engineSource": "https://crates.io/crates/fullbleed/2.5.11",
|
|
5
5
|
"target": "wasm32-wasip1",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"files": {
|
|
46
46
|
"dist/engine.wasm": {
|
|
47
47
|
"bytes": 5585689,
|
|
48
|
-
"sha256": "
|
|
48
|
+
"sha256": "cf8b2ad0493ee32a86871341805c1a5284ad6d2c14bb395593efa4a7c2db2cec"
|
|
49
49
|
},
|
|
50
50
|
"dist/THIRD_PARTY_NOTICES.txt": {
|
|
51
51
|
"bytes": 81572,
|
|
@@ -101,15 +101,15 @@
|
|
|
101
101
|
},
|
|
102
102
|
"dist/browser/client.js": {
|
|
103
103
|
"bytes": 12043,
|
|
104
|
-
"sha256": "
|
|
104
|
+
"sha256": "bf508fad7b4a3e2c542a54c088dce513f7b3356a8dd5e4be31cd3f6c73b63682"
|
|
105
105
|
},
|
|
106
106
|
"dist/browser/worker.js": {
|
|
107
107
|
"bytes": 52109,
|
|
108
|
-
"sha256": "
|
|
108
|
+
"sha256": "0fadb05bcb74518a119f78353072633791c2ce9a4e2bbd189e5b6d6e78de6353"
|
|
109
109
|
},
|
|
110
110
|
"dist/browser/asset-manifest.json": {
|
|
111
111
|
"bytes": 2399,
|
|
112
|
-
"sha256": "
|
|
112
|
+
"sha256": "febca55c5dc4212ec745090dd55b89abffab753df6159e0699ce3e85c2b41a4e"
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
}
|
package/dist/engine.wasm
CHANGED
|
Binary file
|
package/dist/index.d.cts
CHANGED
|
@@ -10,7 +10,7 @@ export interface RenderOptions {
|
|
|
10
10
|
assets?: Readonly<Record<string, Uint8Array>>;
|
|
11
11
|
/** 0 disables previews (default); otherwise 36–300 DPI. */
|
|
12
12
|
previewDpi?: number;
|
|
13
|
-
/** Includes initial engine loading. Default: 30000 ms. */
|
|
13
|
+
/** Includes queue wait and initial engine loading. Default: 30000 ms. */
|
|
14
14
|
timeoutMs?: number;
|
|
15
15
|
/** Reject documents larger than this. Default: 1000. */
|
|
16
16
|
maxPages?: number;
|
|
@@ -37,5 +37,23 @@ export class FullbleedError extends Error {
|
|
|
37
37
|
constructor(code: string, message: string, options?: ErrorOptions);
|
|
38
38
|
}
|
|
39
39
|
export function renderPdf(options: RenderOptions): Promise<RenderResult>;
|
|
40
|
+
export interface RenderQueueOptions {
|
|
41
|
+
/** Maximum simultaneous renders in this queue. Default: 1. */
|
|
42
|
+
concurrency?: number;
|
|
43
|
+
/** Maximum waiting requests, excluding active renders. Default: 16; 0 rejects instead of waiting. */
|
|
44
|
+
maxQueue?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface RenderQueue {
|
|
47
|
+
/** Render in FIFO start order, or reject with QUEUE_FULL/QUEUE_CLOSED. Inputs are copied at admission. */
|
|
48
|
+
renderPdf(options: RenderOptions): Promise<RenderResult>;
|
|
49
|
+
/** Stop admission, cancel accepted jobs and await worker/process cleanup. Idempotent. */
|
|
50
|
+
close(): Promise<void>;
|
|
51
|
+
readonly concurrency: number;
|
|
52
|
+
readonly maxQueue: number;
|
|
53
|
+
readonly activeCount: number;
|
|
54
|
+
readonly pendingCount: number;
|
|
55
|
+
readonly closed: boolean;
|
|
56
|
+
}
|
|
57
|
+
export function createRenderQueue(options?: RenderQueueOptions): RenderQueue;
|
|
40
58
|
export const engineVersion: string;
|
|
41
59
|
export const version: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullbleed",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Generate PDFs from HTML and CSS in Node.js or a browser worker with the Fullbleed Rust engine, bundled fonts, and optional PNG previews.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.cjs",
|
package/src/index.cjs
CHANGED
|
@@ -9,6 +9,7 @@ const { pathToFileURL } = require('node:url');
|
|
|
9
9
|
const pkg = require('../package.json');
|
|
10
10
|
|
|
11
11
|
const { FullbleedError, normalize } = require('./render-input.cjs');
|
|
12
|
+
const { createQueue } = require('./render-queue.cjs');
|
|
12
13
|
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
13
14
|
const root = join(__dirname, '..');
|
|
14
15
|
let runtimePromise;
|
|
@@ -156,6 +157,11 @@ async function renderPdf(options) {
|
|
|
156
157
|
if (Number(process.versions.node.split('.')[0]) < 22) throw new FullbleedError('UNSUPPORTED_NODE', 'Fullbleed requires Node.js 22 or newer.');
|
|
157
158
|
const start = Date.now();
|
|
158
159
|
const job = normalize(options);
|
|
160
|
+
return renderJob(job, start);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function renderJob(job, start) {
|
|
164
|
+
if (Number(process.versions.node.split('.')[0]) < 22) throw new FullbleedError('UNSUPPORTED_NODE', 'Fullbleed requires Node.js 22 or newer.');
|
|
159
165
|
const aborted = () => new FullbleedError('ABORTED', 'PDF rendering was aborted.');
|
|
160
166
|
if (job.signal?.aborted) throw aborted();
|
|
161
167
|
const remaining = job.timeoutMs - (Date.now() - start);
|
|
@@ -211,6 +217,7 @@ async function renderPdf(options) {
|
|
|
211
217
|
}
|
|
212
218
|
|
|
213
219
|
exports.renderPdf = renderPdf;
|
|
220
|
+
exports.createRenderQueue = options => createQueue(renderJob, options);
|
|
214
221
|
exports.FullbleedError = FullbleedError;
|
|
215
222
|
exports.engineVersion = pkg.fullbleed.engineVersion;
|
|
216
223
|
exports.version = pkg.version;
|
package/src/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface RenderOptions {
|
|
|
10
10
|
assets?: Readonly<Record<string, Uint8Array>>;
|
|
11
11
|
/** 0 disables previews (default); otherwise 36–300 DPI. */
|
|
12
12
|
previewDpi?: number;
|
|
13
|
-
/** Includes initial engine loading. Default: 30000 ms. */
|
|
13
|
+
/** Includes queue wait and initial engine loading. Default: 30000 ms. */
|
|
14
14
|
timeoutMs?: number;
|
|
15
15
|
/** Reject documents larger than this. Default: 1000. */
|
|
16
16
|
maxPages?: number;
|
|
@@ -37,5 +37,23 @@ export class FullbleedError extends Error {
|
|
|
37
37
|
constructor(code: string, message: string, options?: ErrorOptions);
|
|
38
38
|
}
|
|
39
39
|
export function renderPdf(options: RenderOptions): Promise<RenderResult>;
|
|
40
|
+
export interface RenderQueueOptions {
|
|
41
|
+
/** Maximum simultaneous renders in this queue. Default: 1. */
|
|
42
|
+
concurrency?: number;
|
|
43
|
+
/** Maximum waiting requests, excluding active renders. Default: 16; 0 rejects instead of waiting. */
|
|
44
|
+
maxQueue?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface RenderQueue {
|
|
47
|
+
/** Render in FIFO start order, or reject with QUEUE_FULL/QUEUE_CLOSED. Inputs are copied at admission. */
|
|
48
|
+
renderPdf(options: RenderOptions): Promise<RenderResult>;
|
|
49
|
+
/** Stop admission, cancel accepted jobs and await worker/process cleanup. Idempotent. */
|
|
50
|
+
close(): Promise<void>;
|
|
51
|
+
readonly concurrency: number;
|
|
52
|
+
readonly maxQueue: number;
|
|
53
|
+
readonly activeCount: number;
|
|
54
|
+
readonly pendingCount: number;
|
|
55
|
+
readonly closed: boolean;
|
|
56
|
+
}
|
|
57
|
+
export function createRenderQueue(options?: RenderQueueOptions): RenderQueue;
|
|
40
58
|
export const engineVersion: string;
|
|
41
59
|
export const version: string;
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
'use strict';
|
|
3
|
+
const { FullbleedError, normalize } = require('./render-input.cjs');
|
|
4
|
+
|
|
5
|
+
const failure = (code, message) => new FullbleedError(code, message);
|
|
6
|
+
const closedError = () => failure('QUEUE_CLOSED', 'The rendering queue is closed.');
|
|
7
|
+
const abortedError = () => failure('ABORTED', 'PDF rendering was aborted.');
|
|
8
|
+
|
|
9
|
+
function createQueue(run, options = {}) {
|
|
10
|
+
const invalid = message => { throw failure('INVALID_INPUT', message); };
|
|
11
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) invalid('Pass queue options as an object.');
|
|
12
|
+
for (const key of Object.keys(options)) {
|
|
13
|
+
if (key !== 'concurrency' && key !== 'maxQueue') invalid(`Unknown queue option: ${key}`);
|
|
14
|
+
}
|
|
15
|
+
const { concurrency = 1, maxQueue = 16 } = options;
|
|
16
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1) invalid('concurrency must be a positive safe integer.');
|
|
17
|
+
if (!Number.isSafeInteger(maxQueue) || maxQueue < 0) invalid('maxQueue must be a nonnegative safe integer.');
|
|
18
|
+
const pending = [];
|
|
19
|
+
const active = new Set();
|
|
20
|
+
let closed = false, closePromise, resolveClose;
|
|
21
|
+
|
|
22
|
+
function finishClose() {
|
|
23
|
+
if (closed && active.size === 0 && pending.length === 0) resolveClose();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function cleanup(entry) {
|
|
27
|
+
clearTimeout(entry.timer);
|
|
28
|
+
entry.sourceSignal?.removeEventListener('abort', entry.onAbort);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cancel(entry, error) {
|
|
32
|
+
if (entry.state === 'settled') return;
|
|
33
|
+
if (entry.state === 'active') {
|
|
34
|
+
entry.stopError ??= error;
|
|
35
|
+
entry.controller.abort();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
pending.splice(pending.indexOf(entry), 1);
|
|
39
|
+
entry.state = 'settled';
|
|
40
|
+
cleanup(entry);
|
|
41
|
+
entry.reject(error);
|
|
42
|
+
pump();
|
|
43
|
+
finishClose();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function complete(entry, error, result) {
|
|
47
|
+
// run() settles only after its worker or process exits. Keep the slot
|
|
48
|
+
// occupied through that cleanup, including failure and cancellation.
|
|
49
|
+
active.delete(entry);
|
|
50
|
+
entry.state = 'settled';
|
|
51
|
+
cleanup(entry);
|
|
52
|
+
error = entry.stopError ?? error;
|
|
53
|
+
if (error) entry.reject(error); else entry.resolve(result);
|
|
54
|
+
pump();
|
|
55
|
+
finishClose();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function pump() {
|
|
59
|
+
if (closed) return;
|
|
60
|
+
while (active.size < concurrency && pending.length) {
|
|
61
|
+
const entry = pending.shift();
|
|
62
|
+
entry.state = 'active';
|
|
63
|
+
clearTimeout(entry.timer);
|
|
64
|
+
active.add(entry);
|
|
65
|
+
Promise.resolve().then(() => run(entry.job, entry.start)).then(
|
|
66
|
+
result => complete(entry, null, result),
|
|
67
|
+
error => complete(entry, error),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function admissionError() {
|
|
73
|
+
if (closed) return closedError();
|
|
74
|
+
if (active.size >= concurrency && pending.length >= maxQueue) {
|
|
75
|
+
return failure('QUEUE_FULL', 'The rendering queue has no available slot.');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderPdf(options) {
|
|
80
|
+
let error = admissionError();
|
|
81
|
+
if (error) return Promise.reject(error);
|
|
82
|
+
const start = Date.now();
|
|
83
|
+
let job;
|
|
84
|
+
try { job = normalize(options); } catch (error) { return Promise.reject(error); }
|
|
85
|
+
// Getters on the input can invoke application code during normalization.
|
|
86
|
+
// Recheck admission before retaining the copied document buffers.
|
|
87
|
+
error = admissionError();
|
|
88
|
+
if (error) return Promise.reject(error);
|
|
89
|
+
if (job.signal?.aborted) return Promise.reject(abortedError());
|
|
90
|
+
const remaining = job.timeoutMs - (Date.now() - start);
|
|
91
|
+
if (remaining <= 0) return Promise.reject(failure('TIMEOUT', `PDF rendering exceeded ${job.timeoutMs} ms.`));
|
|
92
|
+
const sourceSignal = job.signal;
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
job.signal = controller.signal;
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const entry = { job, start, sourceSignal, controller, resolve, reject, state: 'pending' };
|
|
97
|
+
entry.onAbort = () => cancel(entry, abortedError());
|
|
98
|
+
pending.push(entry);
|
|
99
|
+
entry.timer = setTimeout(() => cancel(entry,
|
|
100
|
+
failure('TIMEOUT', `PDF rendering exceeded ${job.timeoutMs} ms.`)), remaining);
|
|
101
|
+
sourceSignal?.addEventListener('abort', entry.onAbort, { once: true });
|
|
102
|
+
if (sourceSignal?.aborted) entry.onAbort();
|
|
103
|
+
else pump();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function close() {
|
|
108
|
+
if (closed) return closePromise;
|
|
109
|
+
closed = true;
|
|
110
|
+
closePromise = new Promise(resolve => { resolveClose = resolve; });
|
|
111
|
+
for (const entry of [...pending, ...active]) cancel(entry, closedError());
|
|
112
|
+
finishClose();
|
|
113
|
+
return closePromise;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return Object.freeze({
|
|
117
|
+
renderPdf, close, concurrency, maxQueue,
|
|
118
|
+
get activeCount() { return active.size; },
|
|
119
|
+
get pendingCount() { return pending.length; },
|
|
120
|
+
get closed() { return closed; },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { createQueue };
|