diff --git a/.gitattributes b/.gitattributes
index cd53f53..c172f16 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -39,3 +39,7 @@ examples/arabic-invoice/fonts/*.ttf binary
examples/arabic-invoice/fonts/OFL.txt -text -whitespace
docs/assets/arabic-invoice/** -text
docs/assets/arabic-invoice/*.pdf binary
+examples/node/** text eol=lf
+examples/node-queue/** text eol=lf
+docs/assets/node-queue/** -text
+docs/assets/node-queue/*.pdf binary
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index d9ff7b4..8438308 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -257,6 +257,22 @@ jobs:
output/reference-env/bin/python -I tools/verify_quickstarts.py
- name: Verify the downloadable Node invoice and guide
run: python tools/verify_node_examples.py
+ - name: Verify the Node queue download and guide
+ run: |
+ python -m pip install pypdf==6.19.0 pypdfium2==5.14.0
+ python tools/build_node_queue.py
+ git diff --exit-code -- docs/assets/node-queue/project.zip docs/assets/node-queue/source.json
+ python tools/verify_node_queue.py
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: node-queue-verification
+ path: |
+ output/node-queue-verification/*.json
+ output/node-queue-verification/*.txt
+ output/node-queue-verification/**/*.pdf
+ output/node-queue-verification/**/*.png
+ retention-days: 14
- name: Verify the downloadable Python web starter and actual HTTP servers
run: python tools/verify_python_web_starter.py --docker
- uses: actions/upload-artifact@v4
@@ -333,6 +349,14 @@ jobs:
retention-days: 14
- name: Build the downloadable browser starter and test its static demo
run: |
+ sudo python3 - <<'PY'
+ from pathlib import Path
+ path = Path('/etc/apt/apt-mirrors.txt')
+ if path.is_file():
+ lines = path.read_text().splitlines()
+ if any(line.startswith('https://archive.ubuntu.com/ubuntu') for line in lines):
+ path.write_text('\n'.join(line for line in lines if 'azure.archive.ubuntu.com' not in line) + '\n')
+ PY
python -m pip install -r tools/browser-requirements.txt
python -m playwright install --with-deps chrome
python tools/build_browser_starter.py
diff --git a/docs/assets/node-queue/NS-1042.pdf b/docs/assets/node-queue/NS-1042.pdf
new file mode 100644
index 0000000..caf8fe6
Binary files /dev/null and b/docs/assets/node-queue/NS-1042.pdf differ
diff --git a/docs/assets/node-queue/NS-1042.png b/docs/assets/node-queue/NS-1042.png
new file mode 100644
index 0000000..5918b8c
Binary files /dev/null and b/docs/assets/node-queue/NS-1042.png differ
diff --git a/docs/assets/node-queue/NS-1043.pdf b/docs/assets/node-queue/NS-1043.pdf
new file mode 100644
index 0000000..53e25d2
Binary files /dev/null and b/docs/assets/node-queue/NS-1043.pdf differ
diff --git a/docs/assets/node-queue/NS-1044.pdf b/docs/assets/node-queue/NS-1044.pdf
new file mode 100644
index 0000000..ed703f8
Binary files /dev/null and b/docs/assets/node-queue/NS-1044.pdf differ
diff --git a/docs/assets/node-queue/NS-1045.pdf b/docs/assets/node-queue/NS-1045.pdf
new file mode 100644
index 0000000..fb1d007
Binary files /dev/null and b/docs/assets/node-queue/NS-1045.pdf differ
diff --git a/docs/assets/node-queue/NS-1046.pdf b/docs/assets/node-queue/NS-1046.pdf
new file mode 100644
index 0000000..fd60186
Binary files /dev/null and b/docs/assets/node-queue/NS-1046.pdf differ
diff --git a/docs/assets/node-queue/NS-1047.pdf b/docs/assets/node-queue/NS-1047.pdf
new file mode 100644
index 0000000..01d4d8e
Binary files /dev/null and b/docs/assets/node-queue/NS-1047.pdf differ
diff --git a/docs/assets/node-queue/project.zip b/docs/assets/node-queue/project.zip
new file mode 100644
index 0000000..5fc995a
Binary files /dev/null and b/docs/assets/node-queue/project.zip differ
diff --git a/docs/assets/node-queue/source.json b/docs/assets/node-queue/source.json
new file mode 100644
index 0000000..712e341
--- /dev/null
+++ b/docs/assets/node-queue/source.json
@@ -0,0 +1,59 @@
+{
+ "package_version": "0.4.0",
+ "engine_version": "2.5.11",
+ "source_commit": "807e4d4cd124980bcc9344a316c47ae3928160b5",
+ "package_url": "https://registry.npmjs.org/fullbleed/-/fullbleed-0.4.0.tgz",
+ "package_integrity": "sha512-dvgpKB8dOtf1Nb7TAQF1EKhC3A34qYcJeM5i86XFww93k7Krm0LIsa4u/UntK1lrCZ+bWj/NveenTNhrejpomQ==",
+ "package_sha256": "fa653f7cc1396cfaf1710656dac7fa37269453263e6b29878a5a3385531d6eb4",
+ "rendered": {
+ "NS-1042.pdf": "3078d155f30536f5ad721f7be3c241cd78661ae41249131fc0fd65f37f23e408",
+ "NS-1042.png": "72420749ccc3deaa8025daf7baffcdf79be716b6e8e93ed605effebed01e86c7",
+ "NS-1043.pdf": "4adda7f7e56df435c40d9a1c1fc3546e2493289d8874ec26a7a16e4ed19b8f44",
+ "NS-1044.pdf": "3d9de4b2366ad977a8bb527cfd20e8d9e62382cd73fb49b651da3a2627d5c56d",
+ "NS-1045.pdf": "a48c1120789cb9b593700786103a7494a96d84cffd9196a2cd0dd0fac3208063",
+ "NS-1046.pdf": "eeabaf8070ef7fdc3d87728bedab9b8e5881a9994a7c5784a3bf9ba844ba9a3b",
+ "NS-1047.pdf": "334dfd5d0213f8a34b9b02eb6b95d11817f6395e99ebbfa0e8224dde06e86dce"
+ },
+ "fictional_data": true,
+ "license": "MIT",
+ "verification_scope": "Six fictional invoices, queue worker limits, guide snippet, and exact replay using the public npm package.",
+ "files": [
+ {
+ "path": "LICENSE",
+ "bytes": 1079,
+ "sha256": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9"
+ },
+ {
+ "path": "README.md",
+ "bytes": 1388,
+ "sha256": "4950a3cf3be6cd930fd5fa18c24c48fee570b7ee13c93255aa584f240e1df4e1"
+ },
+ {
+ "path": "invoice.html",
+ "bytes": 2933,
+ "sha256": "195725eeeb32c521e19b47dbacbb55048bebb6df8566ab135cdc41fbf33fe57a"
+ },
+ {
+ "path": "invoice.css",
+ "bytes": 3546,
+ "sha256": "cda22b39ab1f7a1eeac7cd76d162b05cd3568c2430c099123ba30a2fbd58a296"
+ },
+ {
+ "path": "render.mjs",
+ "bytes": 1484,
+ "sha256": "5a0e9da94bc247b4c2413c251c8386a3f6416b0119c9950a592c93e4aedb2716"
+ },
+ {
+ "path": "package.json",
+ "bytes": 229,
+ "sha256": "194b6b4ac71867f8d56bc4cdee0a2949077f3d90dd6b0e7ebd9417f47fbce2a1"
+ },
+ {
+ "path": "package-lock.json",
+ "bytes": 1177,
+ "sha256": "48b40e800811b0a71be395cf976a384296efaf2d395b0e5411ba446f34a1cc3f"
+ }
+ ],
+ "zip_bytes": 6137,
+ "zip_sha256": "4e96bab39784c58eacefb4e4b368862a948eda5f6b4af90b2b78d248925141e9"
+}
diff --git a/docs/assets/node-queue/verification.json b/docs/assets/node-queue/verification.json
new file mode 100644
index 0000000..8ffb682
--- /dev/null
+++ b/docs/assets/node-queue/verification.json
@@ -0,0 +1,260 @@
+{
+ "schema": "fullbleed.node-queue-example-verification.v1",
+ "ok": true,
+ "package_version": "0.4.0",
+ "engine_version": "2.5.11",
+ "workers": {
+ "created": 6,
+ "peak": 2,
+ "remaining": 0,
+ "node": "v22.23.3",
+ "platform": "win32"
+ },
+ "readers": [
+ "pypdf",
+ "PDFium"
+ ],
+ "commands": [
+ {
+ "name": "install",
+ "exit_code": 0
+ },
+ {
+ "name": "batch",
+ "exit_code": 0
+ },
+ {
+ "name": "replay",
+ "exit_code": 0
+ },
+ {
+ "name": "guide-snippet",
+ "exit_code": 0
+ }
+ ],
+ "project_zip_sha256": "4e96bab39784c58eacefb4e4b368862a948eda5f6b4af90b2b78d248925141e9",
+ "outputs": {
+ "NS-1042.pdf": "3078d155f30536f5ad721f7be3c241cd78661ae41249131fc0fd65f37f23e408",
+ "NS-1042.png": "72420749ccc3deaa8025daf7baffcdf79be716b6e8e93ed605effebed01e86c7",
+ "NS-1043.pdf": "4adda7f7e56df435c40d9a1c1fc3546e2493289d8874ec26a7a16e4ed19b8f44",
+ "NS-1044.pdf": "3d9de4b2366ad977a8bb527cfd20e8d9e62382cd73fb49b651da3a2627d5c56d",
+ "NS-1045.pdf": "a48c1120789cb9b593700786103a7494a96d84cffd9196a2cd0dd0fac3208063",
+ "NS-1046.pdf": "eeabaf8070ef7fdc3d87728bedab9b8e5881a9994a7c5784a3bf9ba844ba9a3b",
+ "NS-1047.pdf": "334dfd5d0213f8a34b9b02eb6b95d11817f6395e99ebbfa0e8224dde06e86dce"
+ },
+ "checks": [
+ {
+ "name": "download ZIP matches the source manifest",
+ "passed": true
+ },
+ {
+ "name": "ZIP contains exactly the reviewed files",
+ "passed": true
+ },
+ {
+ "name": "source and ZIP bytes: LICENSE",
+ "passed": true
+ },
+ {
+ "name": "source and ZIP bytes: README.md",
+ "passed": true
+ },
+ {
+ "name": "source and ZIP bytes: invoice.html",
+ "passed": true
+ },
+ {
+ "name": "source and ZIP bytes: invoice.css",
+ "passed": true
+ },
+ {
+ "name": "source and ZIP bytes: render.mjs",
+ "passed": true
+ },
+ {
+ "name": "source and ZIP bytes: package.json",
+ "passed": true
+ },
+ {
+ "name": "source and ZIP bytes: package-lock.json",
+ "passed": true
+ },
+ {
+ "name": "install command succeeds",
+ "passed": true
+ },
+ {
+ "name": "installed version and engine match the guide",
+ "passed": true
+ },
+ {
+ "name": "lockfile integrity matches the verified release",
+ "passed": true
+ },
+ {
+ "name": "batch command succeeds",
+ "passed": true
+ },
+ {
+ "name": "six distinct invoices rendered without missing glyphs",
+ "passed": true
+ },
+ {
+ "name": "actual worker concurrency stays at two and all workers exit",
+ "passed": true
+ },
+ {
+ "name": "NS-1042.pdf: one PDF page",
+ "passed": true
+ },
+ {
+ "name": "NS-1042.pdf: expected fields in pypdf",
+ "passed": true
+ },
+ {
+ "name": "NS-1042.pdf: expected fields in PDFium",
+ "passed": true
+ },
+ {
+ "name": "NS-1043.pdf: one PDF page",
+ "passed": true
+ },
+ {
+ "name": "NS-1043.pdf: expected fields in pypdf",
+ "passed": true
+ },
+ {
+ "name": "NS-1043.pdf: expected fields in PDFium",
+ "passed": true
+ },
+ {
+ "name": "NS-1044.pdf: one PDF page",
+ "passed": true
+ },
+ {
+ "name": "NS-1044.pdf: expected fields in pypdf",
+ "passed": true
+ },
+ {
+ "name": "NS-1044.pdf: expected fields in PDFium",
+ "passed": true
+ },
+ {
+ "name": "NS-1045.pdf: one PDF page",
+ "passed": true
+ },
+ {
+ "name": "NS-1045.pdf: expected fields in pypdf",
+ "passed": true
+ },
+ {
+ "name": "NS-1045.pdf: expected fields in PDFium",
+ "passed": true
+ },
+ {
+ "name": "NS-1046.pdf: one PDF page",
+ "passed": true
+ },
+ {
+ "name": "NS-1046.pdf: expected fields in pypdf",
+ "passed": true
+ },
+ {
+ "name": "NS-1046.pdf: expected fields in PDFium",
+ "passed": true
+ },
+ {
+ "name": "NS-1047.pdf: one PDF page",
+ "passed": true
+ },
+ {
+ "name": "NS-1047.pdf: expected fields in pypdf",
+ "passed": true
+ },
+ {
+ "name": "NS-1047.pdf: expected fields in PDFium",
+ "passed": true
+ },
+ {
+ "name": "NS-1042.pdf: matches reviewed output",
+ "passed": true
+ },
+ {
+ "name": "NS-1042.png: matches reviewed output",
+ "passed": true
+ },
+ {
+ "name": "NS-1043.pdf: matches reviewed output",
+ "passed": true
+ },
+ {
+ "name": "NS-1044.pdf: matches reviewed output",
+ "passed": true
+ },
+ {
+ "name": "NS-1045.pdf: matches reviewed output",
+ "passed": true
+ },
+ {
+ "name": "NS-1046.pdf: matches reviewed output",
+ "passed": true
+ },
+ {
+ "name": "NS-1047.pdf: matches reviewed output",
+ "passed": true
+ },
+ {
+ "name": "replay command succeeds",
+ "passed": true
+ },
+ {
+ "name": "exact-input replay preserves every PDF and PNG",
+ "passed": true
+ },
+ {
+ "name": "guide contains one executable JavaScript example",
+ "passed": true
+ },
+ {
+ "name": "guide-snippet command succeeds",
+ "passed": true
+ },
+ {
+ "name": "invoice-1042.pdf: one PDF page",
+ "passed": true
+ },
+ {
+ "name": "invoice-1042.pdf: expected fields in pypdf",
+ "passed": true
+ },
+ {
+ "name": "invoice-1042.pdf: expected fields in PDFium",
+ "passed": true
+ },
+ {
+ "name": "invoice-1043.pdf: one PDF page",
+ "passed": true
+ },
+ {
+ "name": "invoice-1043.pdf: expected fields in pypdf",
+ "passed": true
+ },
+ {
+ "name": "invoice-1043.pdf: expected fields in PDFium",
+ "passed": true
+ },
+ {
+ "name": "invoice-1044.pdf: one PDF page",
+ "passed": true
+ },
+ {
+ "name": "invoice-1044.pdf: expected fields in pypdf",
+ "passed": true
+ },
+ {
+ "name": "invoice-1044.pdf: expected fields in PDFium",
+ "passed": true
+ }
+ ],
+ "scope": "This downloaded batch, guide snippet, and exact replay; no throughput or memory-capacity claim."
+}
diff --git a/docs/assets/node/project.json b/docs/assets/node/project.json
index 3519ad1..63bcb29 100644
--- a/docs/assets/node/project.json
+++ b/docs/assets/node/project.json
@@ -1,10 +1,10 @@
{
- "package_version": "0.3.2",
+ "package_version": "0.4.0",
"engine_version": "2.5.11",
- "source_commit": "38963536427d85e347182b3b8141d0a84c855a26",
- "package_url": "https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.3.2/fullbleed-0.3.2.tgz",
- "package_integrity": "sha512-bzbTUn6Hcxu0YZoP38wkrJ/nZ33+rQfZq5itdpQUl7lIO9mR1m9LC5n/2KLxjj+7dAb8IhpYoVTSmbuhQyA5Vg==",
- "package_sha256": "536a18eb10847326cc9cbe19ef88e41201230e655a0e7bc69cadbce41b85b971",
+ "source_commit": "807e4d4cd124980bcc9344a316c47ae3928160b5",
+ "package_url": "https://registry.npmjs.org/fullbleed/-/fullbleed-0.4.0.tgz",
+ "package_integrity": "sha512-dvgpKB8dOtf1Nb7TAQF1EKhC3A34qYcJeM5i86XFww93k7Krm0LIsa4u/UntK1lrCZ+bWj/NveenTNhrejpomQ==",
+ "package_sha256": "fa653f7cc1396cfaf1710656dac7fa37269453263e6b29878a5a3385531d6eb4",
"rendered": {
"invoice.pdf": "3078d155f30536f5ad721f7be3c241cd78661ae41249131fc0fd65f37f23e408",
"invoice.png": "72420749ccc3deaa8025daf7baffcdf79be716b6e8e93ed605effebed01e86c7"
@@ -27,7 +27,7 @@
},
"README.md": {
"bytes": 903,
- "sha256": "ca7d2154978a19fc42953f1331bbbad68a539ad3fa67050e7a3a66ae063a9bed"
+ "sha256": "b09ca9bf312fbe1a9f9ddb10cfc535459057139ac56aa0cbc55a6f795ac3d54d"
},
"invoice.css": {
"bytes": 3546,
@@ -39,16 +39,16 @@
},
"package-lock.json": {
"bytes": 1165,
- "sha256": "d657eb89aa32485c8eaf3a8f55074007ad1b0f6b2a25e9ceb7d1069465422751"
+ "sha256": "b4f1d91458aca95102e680a5fd3491aef1e78f3fd1244ed472295a4456a5ab9f"
},
"package.json": {
"bytes": 241,
- "sha256": "80ed17d4ef2ca8261b456861a1ad991859c2dd9dc761b3540d704cac16476ca3"
+ "sha256": "b8107cbf1978becb2fdf684993e83242bd57b63d058d1195366b75b5fa4366af"
},
"render.mjs": {
"bytes": 1064,
"sha256": "dd63e83658800df2672c8c5ca1b176dbe49fc0d5c49b8c2b79af91515a68e776"
}
},
- "zip_sha256": "8ea7f43108b4d54203d89870902e7da398ae692d413dbdebbe8129eaaf1bac5b"
+ "zip_sha256": "a23694b044ce863a41222daf80a7955c242c4b4634d1d8af200dd1911814a21e"
}
diff --git a/docs/assets/node/project.zip b/docs/assets/node/project.zip
index 7c9d1bc..6085c0a 100644
Binary files a/docs/assets/node/project.zip and b/docs/assets/node/project.zip differ
diff --git a/docs/examples.md b/docs/examples.md
index 03b2c6e..f8e7242 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -40,6 +40,8 @@ their own roles. Grid and table layouts keep the page ordered.
Start with the [invoice guide](guides/invoices.md), or
[serve an invoice from FastAPI, Flask, or Django](guides/web-frameworks.md).
The Python starter also includes a [tested Docker image](guides/python-docker.md).
+For Node applications, download the [six-invoice queue project](guides/node-render-queue.md)
+with editable HTML/CSS and limits on active and waiting renders.
For C# web applications, try the [ASP.NET Core download starter](guides/aspnet-pdf.md).
For a tagged document, use the [editable C# library notice](guides/csharp-tagged-pdf.md)
with PDF/UA profiles, embedded fonts and retained validation reports.
diff --git a/docs/getting-started/node.md b/docs/getting-started/node.md
index d3087e5..0f1a836 100644
--- a/docs/getting-started/node.md
+++ b/docs/getting-started/node.md
@@ -8,14 +8,13 @@ Fullbleed's optional Node package renders static HTML/CSS into PDFs using a
bundled WebAssembly build of the Rust engine. It includes fonts and runs locally;
you do not need Python, Rust, or a browser to use the installed package.
-This guide uses **Node package 0.3.2**, powered by **Fullbleed 2.5.11**. The
+This guide uses **Node package 0.4.0**, powered by **Fullbleed 2.5.11**. The
integration and core engine have separate versions.
-Version 0.3.2 fixes blank PNG previews for unembedded Helvetica, Times, and
-Courier faces by bundling fixed, OFL-licensed outline substitutes. It retains
-the inline-layout corrections from 0.3.1. Optional process
-isolation lets a failed render child reject its request while the caller keeps
-running; see the server guidance below.
+Version 0.4.0 adds a shared render queue with limits on active and waiting jobs.
+It retains bundled outline substitutes for Helvetica, Times, and Courier PNG
+previews, inline-layout corrections, and optional process isolation. See the
+server guidance below for queue limits and failure handling.
Use the actual family name for normal/italic CSS selection, or a PostScript name
for an explicit face. Review appearance and line breaks when upgrading; the
@@ -31,7 +30,7 @@ With Node.js 22 or newer, install the [npm package](https://www.npmjs.com/packag
npm install fullbleed
```
-To pin this guide's version, use `npm install --save-exact fullbleed@0.3.2`.
+To pin this guide's version, use `npm install --save-exact fullbleed@0.4.0`.
The npm package contains the same verified bytes as the GitHub release.
Save this as `invoice.mjs` and run `node invoice.mjs`:
@@ -89,7 +88,7 @@ of the finalized PDF. Fullbleed 2.5.5 fixes gradient fills that were missing fro
these PNG previews in earlier releases.
[Open the report PDF](../assets/node/report.pdf){ .md-button }
-[Explore its HTML and CSS](https://github.com/fullbleed-engine/fullbleed-node/tree/v0.3.2/examples){ .md-button }
+[Explore its HTML and CSS](https://github.com/fullbleed-engine/fullbleed-node/tree/v0.4.0/examples){ .md-button }
## Bring fonts and image assets
@@ -100,14 +99,14 @@ in CSS. Pass images through `assets`, mapping relative names to `Buffer` or
The engine uses an in-memory filesystem for the supplied inputs and does not
fetch remote URLs. Data URIs also work for document images. The
-[package API reference](https://github.com/fullbleed-engine/fullbleed-node/tree/v0.3.2#fonts-and-assets)
+[package API reference](https://github.com/fullbleed-engine/fullbleed-node/tree/v0.4.0#fonts-and-assets)
includes a complete font and asset example.
Unembedded PDF Standard 14 faces now have bundled substitutes for native PNG
previews. The PDF keeps its original font resources, text, and advances; a PDF
reader can use a different substitute design. Register your chosen font when
the exact branded appearance matters. See the
-[preview comparison and font limits](https://github.com/fullbleed-engine/fullbleed-node/blob/v0.3.2/docs/standard-font-previews.md).
+[preview comparison and font limits](https://github.com/fullbleed-engine/fullbleed-node/blob/v0.4.0/docs/standard-font-previews.md).
## Use it in an application
@@ -125,6 +124,11 @@ Use `timeoutMs`, `maxPages`, and an `AbortSignal` to bound a job. Limit concurre
calls to fit your server's memory; each worker's WebAssembly memory ceiling is
512 MiB. Preview images can use more memory than PDF-only rendering.
+Node package 0.4.0 adds a [shared render queue](../guides/node-render-queue.md)
+with active and waiting limits, deadlines that include queue time, cancellation,
+and shutdown. The guide includes a runnable six-invoice batch. Use one queue per
+application process to share the limits across request handlers.
+
Rendering errors reject with a `FullbleedError` and a code such as `INVALID_INPUT`,
`MISSING_GLYPHS`, `PAGE_LIMIT`, `TIMEOUT`, or `ABORTED`. Invalid font data is
rejected. Use registered font families: unavailable CSS families can fall back,
@@ -156,7 +160,7 @@ Process startup adds latency and memory use. There is no process pool or
automatic retry; keep concurrency bounded. Application startup flags and
`NODE_OPTIONS` are not replayed in the child. This option requires a normal
Node executable and is not an operating-system security sandbox. See the
-[process isolation guide](https://github.com/fullbleed-engine/fullbleed-node/blob/v0.3.2/docs/process-isolation.md)
+[process isolation guide](https://github.com/fullbleed-engine/fullbleed-node/blob/v0.4.0/docs/process-isolation.md)
for deployment details and the tested failure paths. The Next.js starter uses
this mode for its PDF route.
@@ -181,18 +185,19 @@ TypeScript. Its release evidence compares retained native and WebAssembly
fixtures and installs the same tarball across Node 22, 24, and 26 on Windows,
Linux, and macOS. These are scoped engineering checks, not PDF standards certification.
-The [public-install verification](https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.3.2/npm-public-verification.json)
+The [public-install verification](https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.4.0/npm-public-verification.json)
records the registry tarball hash, audits npm signatures and provenance, and
-exercises 26 font cases using worker and process isolation. Their PDFs and PNGs
-match the CI output. The
-[release verification](https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.3.2/release-verification.json)
-retains 32 native/Wasm comparisons, nine installed Node/platform combinations,
-and 105 checks in each of Chrome, Firefox, and Playwright WebKit, including
-browser downloads and failure recovery. The invoice and report on this page
+executes the queue lifecycle checks from a fresh registry install. Its queued
+PDF and PNG match the CI output. The
+[release verification](https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.4.0/release-verification.json)
+retains 32 native/Wasm comparisons, fourteen queue checks in each of nine
+installed Node/platform combinations, and 105 checks in each of Chrome,
+Firefox, and Playwright WebKit, including browser downloads and failure
+recovery. The invoice and report on this page
retain the reviewed 2.5.10 PDF bytes and preview pixels with engine 2.5.11;
the [download manifest](../assets/node/project.json) records their exact hashes.
[Package source and API](https://github.com/fullbleed-engine/fullbleed-node) ·
[npm package](https://www.npmjs.com/package/fullbleed) ·
-[Versioned release and evidence](https://github.com/fullbleed-engine/fullbleed-node/releases/tag/v0.3.2) ·
+[Versioned release and evidence](https://github.com/fullbleed-engine/fullbleed-node/releases/tag/v0.4.0) ·
[CSS coverage](../css-coverage.md) · [Examples](../examples.md)
diff --git a/docs/guides/node-render-queue.md b/docs/guides/node-render-queue.md
new file mode 100644
index 0000000..92a876d
--- /dev/null
+++ b/docs/guides/node-render-queue.md
@@ -0,0 +1,114 @@
+---
+title: Queue PDF jobs in Node.js
+description: Bound concurrent PDF requests with Fullbleed's Node render queue, waiting limits, cancellation, and deadlines. Download a six-invoice batch project.
+---
+
+# Queue PDF jobs in Node.js
+
+An HTTP request burst or a batch of order events can ask for many PDFs at once.
+Share one `createRenderQueue()` across those callers to limit active renders and
+waiting work. A request beyond those limits rejects with `QUEUE_FULL`, giving
+the application a way to report that it is busy.
+
+This guide uses **Node package 0.4.0**, with the **Fullbleed 2.5.11** engine and
+bundled fonts. The sample submits six fictional invoices to two active slots and
+four waiting slots.
+
+[Download the batch project](../assets/node-queue/project.zip){ .md-button .md-button--primary }
+[Open a generated invoice](../assets/node-queue/NS-1042.pdf){ .md-button }
+
+[](../assets/node-queue/NS-1042.pdf)
+
+## Run the batch
+
+Extract the ZIP and open its `node-pdf-queue` directory. With Node.js 22 or newer:
+
+```sh
+npm ci --ignore-scripts
+npm run render
+```
+
+Open the six PDFs in `output/queued-invoices` and the first invoice's PNG preview.
+Edit the customer names in `render.mjs`, or change the HTML and CSS templates.
+The script escapes customer names before inserting them. The lockfile pins the
+published package and its integrity hash.
+
+## Reuse one queue across callers
+
+In a Node project with `fullbleed@0.4.0` installed, this runnable example writes
+three PDFs. In a server, keep the queue at application scope and call its
+`renderPdf()` method from each request handler:
+
+```javascript
+import { writeFile } from 'node:fs/promises';
+import { createRenderQueue } from 'fullbleed';
+
+const documents = createRenderQueue({ concurrency: 2, maxQueue: 4 });
+try {
+ await Promise.all([1042, 1043, 1044].map(async reference => {
+ const result = await documents.renderPdf({
+ html: `
Invoice NS-${reference}
Consulting: USD 1,200.00
`,
+ css: '@page { size: A4; margin: 20mm } h1 { color: #175c52 }',
+ timeoutMs: 30_000,
+ isolation: 'process',
+ });
+ await writeFile(`invoice-${reference}.pdf`, result.pdf);
+ }));
+} finally {
+ await documents.close();
+}
+```
+
+The defaults are one active render and sixteen waiting requests. Set `maxQueue`
+to zero to reject buffering. Slots remain occupied until each worker or child
+process has exited, including when rendering fails or is cancelled. A failed job
+releases its slot so later work can proceed. The queue never retries jobs.
+
+The `isolation: 'process'` option contains tested child-process failures and
+requires a host that permits child processes. It adds startup and memory
+overhead. The default worker mode also works with the queue; see
+[process isolation](../getting-started/node.md#contain-render-process-failures)
+for the tradeoffs and the unresolved native-crash investigation.
+
+## Waiting, cancellation, and shutdown
+
+`timeoutMs` starts at submission, including time in the queue. A waiting request
+that expires rejects with `TIMEOUT` without starting a worker. Pass an
+`AbortSignal` to cancel waiting or active work. Cancelling a waiting request frees
+its waiting slot immediately; active cancellation waits for cleanup.
+
+Accepted requests copy input asset/font bytes and snapshot HTML/CSS strings, so
+later changes to caller-owned objects do not change a waiting document.
+Read `activeCount` and `pendingCount` to observe the queue.
+
+`close()` stops admission, cancels unfinished accepted jobs with `QUEUE_CLOSED`,
+and resolves after cleanup. It is idempotent. In a server, call it at application
+shutdown rather than after every request. If accepted jobs must finish normally,
+await those jobs first, then close.
+
+## Choose limits for the application
+
+Create one shared queue per application process. Separate queues, standalone
+`renderPdf()` calls, and separate server processes have independent limits.
+The queue bounds job counts, not total memory. Waiting jobs retain their input
+snapshots, and preview images use more memory than PDF-only work.
+
+A web server can map `QUEUE_FULL` to HTTP 503. An automation consumer can wait for
+an accepted job to settle before submitting more. Choose limits for the host and
+document sizes. Use the application's durable job system for order events,
+scheduling, retries, authentication, and storage; this queue is held in memory
+and is lost when the process exits.
+
+## Verification
+
+The [download checks](../assets/node-queue/verification.json) execute the actual
+ZIP and this guide's snippet using the public npm package. They observe the six
+render workers, compare replayed PDF/PNG bytes, and read invoice references,
+customer names, and totals with independent PDF readers. The package's
+[release evidence](https://github.com/fullbleed-engine/fullbleed-node/releases/tag/v0.4.0)
+covers admission, overload, cancellation, deadlines, shutdown, and process-failure
+recovery across the installed-package platform matrix.
+
+[Node quickstart](../getting-started/node.md) ·
+[API and queue details](https://github.com/fullbleed-engine/fullbleed-node/blob/v0.4.0/docs/render-queue.md) ·
+[Next.js PDF downloads](nextjs-pdf.md)
diff --git a/examples/node-queue/LICENSE b/examples/node-queue/LICENSE
new file mode 100644
index 0000000..4c99bbd
--- /dev/null
+++ b/examples/node-queue/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Fullbleed Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/examples/node-queue/README.md b/examples/node-queue/README.md
new file mode 100644
index 0000000..b67b1e5
--- /dev/null
+++ b/examples/node-queue/README.md
@@ -0,0 +1,30 @@
+# Queue six PDF invoices in Node.js
+
+This fictional batch uses Fullbleed's Node package 0.4.0 and its pinned 2.5.11
+engine. Node.js 22 or newer is required. The npm package includes the engine and
+fonts; rendering needs no Python, Rust, browser, or system-font installation.
+
+```sh
+npm ci --ignore-scripts
+npm run render
+```
+
+Open `output/queued-invoices/NS-1042.pdf` through `NS-1047.pdf`, and the first
+invoice's PNG preview. Change the six customer names in `render.mjs`, or edit
+`invoice.html` and `invoice.css`. Customer names are escaped before insertion.
+All example data is fictional.
+
+The shared queue uses two active slots and four waiting slots. The six requests
+fit in that admission limit. Extra simultaneous requests reject with
+`QUEUE_FULL`; submit later work after accepted jobs settle or return a busy
+response to a caller. The queue counts jobs rather than capping total memory.
+
+For a server, create one queue at application scope and reuse it across requests.
+The example closes its queue only after the batch finishes. `close()` cancels
+unfinished jobs and waits for worker/process cleanup. Durable job storage and
+retry policy belong in the surrounding application.
+
+See https://docs.fullbleed.dev/guides/node-render-queue/ for deadlines,
+cancellation, shutdown, and process isolation.
+
+The project is MIT licensed. Font licenses ship with the installed dependency.
diff --git a/examples/node-queue/invoice.css b/examples/node-queue/invoice.css
new file mode 100644
index 0000000..235023f
--- /dev/null
+++ b/examples/node-queue/invoice.css
@@ -0,0 +1,48 @@
+@page { size: A4; margin: 0; }
+* { box-sizing: border-box; }
+html, body { margin: 0; padding: 0; }
+body { font-family: Inter; font-size: 10pt; line-height: 1.45; color: #17382e; }
+h1, h2, h3, p, figure { margin: 0; }
+h1, h2, h3 { font-weight: 400; }
+.page { width: 210mm; height: 296.9mm; padding: 15mm 16mm; position: relative; break-after: page; background: #f8f5ec; }
+.page:last-child { break-after: auto; }
+.kicker { font-size: 7.2pt; font-weight: 600; letter-spacing: 1.4pt; text-transform: uppercase; }
+.serif { font-family: 'DM Serif Display'; font-weight: 400; }
+.display { font-family: 'Bebas Neue'; font-weight: 400; }
+.muted { color: #5b6c64; }
+.small { font-size: 8pt; }
+.rule { border-top: .6pt solid #9eafa4; }
+.row { display: flex; justify-content: space-between; gap: 16pt; }
+.grid-2 { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 24pt; }
+.grid-2 > * { min-width: 0; }
+.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16pt; }
+.footer { position: absolute; left: 16mm; right: 16mm; bottom: 12mm; border-top: .6pt solid #9eafa4; padding-top: 8pt; display: flex; justify-content: space-between; gap: 12pt; font-size: 6.6pt; letter-spacing: .45pt; }
+table { width: 100%; border-collapse: collapse; font-size: 9pt; }
+th { font-size: 7.5pt; font-weight: 600; text-align: left; }
+th, td { padding: 10pt 0; border-bottom: .55pt solid #bdc8bf; vertical-align: top; }
+.number { text-align: right; white-space: nowrap; }
+.money { font-variant-numeric: tabular-nums; }
+strong { font-weight: 600; }
+a { color: inherit; }
+
+.invoice .masthead { display: grid; grid-template-columns: 1.05fr .8fr 1fr; gap: 18pt; align-items: center; padding-bottom: 22pt; border-bottom: 1pt solid #17382e; }
+.invoice .brand { font: 31pt/.88 'DM Serif Display'; letter-spacing: -1.3pt; }
+.invoice .brand span { display: block; line-height: .98; }
+.invoice .brand small { display: block; font: 6.5pt Inter; letter-spacing: 2.1pt; margin-top: 11pt; }
+.invoice .brand-note { border-left: 1pt solid #c24929; padding: 4pt 0 4pt 16pt; line-height: 1.8; }
+.invoice .address { font-size: 7.5pt; line-height: 1.7; border-left: 1pt solid #c24929; padding-left: 16pt; }
+.invoice .title-row { display: grid; grid-template-columns: 3.3fr 1fr; gap: 20pt; align-items: center; padding: 22pt 0 16pt; }
+.invoice h1 { font: 91pt/.9 'Bebas Neue'; letter-spacing: .3pt; }
+.invoice .title-note { color: #b44123; font: 14pt/1.2 'Bebas Neue'; letter-spacing: .5pt; }
+.invoice .details { border-top: .6pt solid #9eafa4; border-bottom: .6pt solid #9eafa4; padding: 15pt 0; margin-bottom: 10pt; }
+.invoice .details p { margin-top: 5pt; font-size: 8.2pt; }
+.invoice .details > div + div { border-left: .6pt solid #c7cdc4; padding-left: 14pt; }
+.invoice th { color: #52665a; }
+.invoice td { padding: 12pt 0; }
+.invoice .item-note { color: #617069; font-size: 8pt; margin-top: 4pt; }
+.invoice .amounts { display: grid; grid-template-columns: 1fr 1.25fr; gap: 27pt; margin-top: 23pt; }
+.invoice .thanks { font: italic 22pt/1.2 'DM Serif Display'; color: #b44123; padding-top: 8pt; }
+.invoice .summary-line { display: flex; justify-content: space-between; padding: 5pt 0; font-size: 8.5pt; }
+.invoice .total { background: #17382e; color: #fff8e8; padding: 16pt; margin-top: 9pt; border-left: 4pt solid #d96238; }
+.invoice .total .value { font: 38pt/1.1 'Bebas Neue'; margin-top: 7pt; }
+.invoice .payment { margin-top: 20pt; padding-top: 12pt; border-top: .6pt solid #9eafa4; display: grid; grid-template-columns: 1fr 1fr; gap: 24pt; font-size: 8pt; }
diff --git a/examples/node-queue/invoice.html b/examples/node-queue/invoice.html
new file mode 100644
index 0000000..09bb52c
--- /dev/null
+++ b/examples/node-queue/invoice.html
@@ -0,0 +1,12 @@
+Northstar Studio — Invoice NS-1042
+
+
+INVOICE
GOOD WORK.
CLEAR DETAILS.
LET'S MAKE
WHAT'S NEXT.
+Prepared for
Maple & Finch
Attn. Avery Morgan
Portland, Oregon
The project
Autumn launch
Brand workshop, digital
design & handoff
Invoice NS-1042
Issued · 01 October 2026
Due · 31 October 2026
Currency · USD
+| DESCRIPTION | HOURS | RATE | AMOUNT |
+| Discovery & brand workshop Positioning, priorities, and a clear creative direction | 8 | $125.00 | $1,000.00 |
| Digital design & implementation A cohesive visual system, from layout to launch | 6 | $95.00 | $570.00 |
| Review & studio handoff Final refinements, documentation, and delivery | 2 | $150.00 | $300.00 |
+
+A little clarity.
A lot of possibility.
Thank you for inviting us
to help shape your next chapter.
Subtotal$1,870.00
Tax · 0% in this sample$0.00
+Payment details
Reference NS-1042 with your payment.
Payment instructions are supplied separately.
A note from the studio
Questions about a line item?
Write to hello@example.invalid.
+
+
diff --git a/examples/node-queue/package-lock.json b/examples/node-queue/package-lock.json
new file mode 100644
index 0000000..d490bb4
--- /dev/null
+++ b/examples/node-queue/package-lock.json
@@ -0,0 +1,39 @@
+{
+ "name": "fullbleed-node-queue-example",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "fullbleed-node-queue-example",
+ "version": "1.0.0",
+ "dependencies": {
+ "fullbleed": "0.4.0"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@bjorn3/browser_wasi_shim": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.4.2.tgz",
+ "integrity": "sha512-/iHkCVUG3VbcbmEHn5iIUpIrh7a7WPiwZ3sHy4HZKZzBdSadwdddYDZAII2zBvQYV0Lfi8naZngPCN7WPHI/hA==",
+ "license": "MIT OR Apache-2.0"
+ },
+ "node_modules/fullbleed": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/fullbleed/-/fullbleed-0.4.0.tgz",
+ "integrity": "sha512-dvgpKB8dOtf1Nb7TAQF1EKhC3A34qYcJeM5i86XFww93k7Krm0LIsa4u/UntK1lrCZ+bWj/NveenTNhrejpomQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@bjorn3/browser_wasi_shim": "0.4.2"
+ },
+ "bin": {
+ "fullbleed-browser-assets": "src/copy-browser-assets.cjs"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ }
+ }
+}
diff --git a/examples/node-queue/package.json b/examples/node-queue/package.json
new file mode 100644
index 0000000..02428e8
--- /dev/null
+++ b/examples/node-queue/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "fullbleed-node-queue-example",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "engines": { "node": ">=22" },
+ "scripts": { "render": "node render.mjs" },
+ "dependencies": { "fullbleed": "0.4.0" }
+}
diff --git a/examples/node-queue/render.mjs b/examples/node-queue/render.mjs
new file mode 100644
index 0000000..493a509
--- /dev/null
+++ b/examples/node-queue/render.mjs
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: MIT
+import { readFile, writeFile, mkdir } from 'node:fs/promises';
+import { join, resolve } from 'node:path';
+import { createRenderQueue } from 'fullbleed';
+
+const template = await readFile(new URL('./invoice.html', import.meta.url), 'utf8');
+const css = await readFile(new URL('./invoice.css', import.meta.url), 'utf8');
+const customers = ['Maple & Finch', 'Cedar Works', 'Fern Design', 'Juniper Press', 'Coast Studio', 'Pine & Paper'];
+const escapeHtml = text => text.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
+const output = resolve(process.argv[2] ?? 'output/queued-invoices');
+await mkdir(output, { recursive: true });
+const documents = createRenderQueue({ concurrency: 2, maxQueue: 4 });
+try {
+ const results = await Promise.all(customers.map(async (customer, index) => {
+ const reference = `NS-${1042 + index}`;
+ const html = template.replaceAll('Maple & Finch', escapeHtml(customer)).replaceAll('NS-1042', reference);
+ const result = await documents.renderPdf({ html, css, previewDpi: index === 0 ? 96 : 0 });
+ await writeFile(join(output, reference + '.pdf'), result.pdf);
+ if (index === 0) await writeFile(join(output, reference + '.png'), result.previews[0]);
+ return { reference, customer, pages: result.pages, missingGlyphs: result.missingGlyphs };
+ }));
+ console.log(JSON.stringify({ output, invoices: results }));
+} finally {
+ await documents.close();
+}
diff --git a/examples/node-queue/source.json b/examples/node-queue/source.json
new file mode 100644
index 0000000..8212c3f
--- /dev/null
+++ b/examples/node-queue/source.json
@@ -0,0 +1,20 @@
+{
+ "package_version": "0.4.0",
+ "engine_version": "2.5.11",
+ "source_commit": "807e4d4cd124980bcc9344a316c47ae3928160b5",
+ "package_url": "https://registry.npmjs.org/fullbleed/-/fullbleed-0.4.0.tgz",
+ "package_integrity": "sha512-dvgpKB8dOtf1Nb7TAQF1EKhC3A34qYcJeM5i86XFww93k7Krm0LIsa4u/UntK1lrCZ+bWj/NveenTNhrejpomQ==",
+ "package_sha256": "fa653f7cc1396cfaf1710656dac7fa37269453263e6b29878a5a3385531d6eb4",
+ "rendered": {
+ "NS-1042.pdf": "3078d155f30536f5ad721f7be3c241cd78661ae41249131fc0fd65f37f23e408",
+ "NS-1042.png": "72420749ccc3deaa8025daf7baffcdf79be716b6e8e93ed605effebed01e86c7",
+ "NS-1043.pdf": "4adda7f7e56df435c40d9a1c1fc3546e2493289d8874ec26a7a16e4ed19b8f44",
+ "NS-1044.pdf": "3d9de4b2366ad977a8bb527cfd20e8d9e62382cd73fb49b651da3a2627d5c56d",
+ "NS-1045.pdf": "a48c1120789cb9b593700786103a7494a96d84cffd9196a2cd0dd0fac3208063",
+ "NS-1046.pdf": "eeabaf8070ef7fdc3d87728bedab9b8e5881a9994a7c5784a3bf9ba844ba9a3b",
+ "NS-1047.pdf": "334dfd5d0213f8a34b9b02eb6b95d11817f6395e99ebbfa0e8224dde06e86dce"
+ },
+ "fictional_data": true,
+ "license": "MIT",
+ "verification_scope": "Six fictional invoices, queue worker limits, guide snippet, and exact replay using the public npm package."
+}
diff --git a/examples/node/README.md b/examples/node/README.md
index f44cfde..d302a92 100644
--- a/examples/node/README.md
+++ b/examples/node/README.md
@@ -1,6 +1,6 @@
# A designed invoice from Node.js
-This fictional Northstar Studio invoice uses Fullbleed's Node package 0.3.2,
+This fictional Northstar Studio invoice uses Fullbleed's Node package 0.4.0,
which bundles the Fullbleed 2.5.11 engine and Inter, DM Serif Display, and Bebas
Neue fonts. You need Node.js 22 or newer; no Python or Rust installation is needed.
@@ -13,7 +13,7 @@ Open `output/invoice/invoice.pdf` and `output/invoice/page-1.png`. Change the
`customer` value in `render.mjs`; the script escapes it before insertion into HTML.
Edit `invoice.html` and `invoice.css` for your own layout and data.
-The lockfile pins Fullbleed 0.3.2 from npm and its integrity hash.
+The lockfile pins Fullbleed 0.4.0 from npm and its integrity hash.
The package renders locally; it does not upload documents to a service.
The sample and engine are MIT licensed. The package retains the bundled font
diff --git a/examples/node/package-lock.json b/examples/node/package-lock.json
index 2e7c52a..13a971f 100644
--- a/examples/node/package-lock.json
+++ b/examples/node/package-lock.json
@@ -8,7 +8,7 @@
"name": "fullbleed-node-invoice",
"version": "1.0.0",
"dependencies": {
- "fullbleed": "0.3.2"
+ "fullbleed": "0.4.0"
},
"engines": {
"node": ">=22"
@@ -21,9 +21,9 @@
"license": "MIT OR Apache-2.0"
},
"node_modules/fullbleed": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/fullbleed/-/fullbleed-0.3.2.tgz",
- "integrity": "sha512-bzbTUn6Hcxu0YZoP38wkrJ/nZ33+rQfZq5itdpQUl7lIO9mR1m9LC5n/2KLxjj+7dAb8IhpYoVTSmbuhQyA5Vg==",
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/fullbleed/-/fullbleed-0.4.0.tgz",
+ "integrity": "sha512-dvgpKB8dOtf1Nb7TAQF1EKhC3A34qYcJeM5i86XFww93k7Krm0LIsa4u/UntK1lrCZ+bWj/NveenTNhrejpomQ==",
"license": "MIT",
"dependencies": {
"@bjorn3/browser_wasi_shim": "0.4.2"
diff --git a/examples/node/package.json b/examples/node/package.json
index 3166a70..5eee503 100644
--- a/examples/node/package.json
+++ b/examples/node/package.json
@@ -10,6 +10,6 @@
"node": ">=22"
},
"dependencies": {
- "fullbleed": "0.3.2"
+ "fullbleed": "0.4.0"
}
}
diff --git a/examples/node/source.json b/examples/node/source.json
index 3f26e18..e41484e 100644
--- a/examples/node/source.json
+++ b/examples/node/source.json
@@ -1,10 +1,10 @@
{
- "package_version": "0.3.2",
+ "package_version": "0.4.0",
"engine_version": "2.5.11",
- "source_commit": "38963536427d85e347182b3b8141d0a84c855a26",
- "package_url": "https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.3.2/fullbleed-0.3.2.tgz",
- "package_integrity": "sha512-bzbTUn6Hcxu0YZoP38wkrJ/nZ33+rQfZq5itdpQUl7lIO9mR1m9LC5n/2KLxjj+7dAb8IhpYoVTSmbuhQyA5Vg==",
- "package_sha256": "536a18eb10847326cc9cbe19ef88e41201230e655a0e7bc69cadbce41b85b971",
+ "source_commit": "807e4d4cd124980bcc9344a316c47ae3928160b5",
+ "package_url": "https://registry.npmjs.org/fullbleed/-/fullbleed-0.4.0.tgz",
+ "package_integrity": "sha512-dvgpKB8dOtf1Nb7TAQF1EKhC3A34qYcJeM5i86XFww93k7Krm0LIsa4u/UntK1lrCZ+bWj/NveenTNhrejpomQ==",
+ "package_sha256": "fa653f7cc1396cfaf1710656dac7fa37269453263e6b29878a5a3385531d6eb4",
"rendered": {
"invoice.pdf": "3078d155f30536f5ad721f7be3c241cd78661ae41249131fc0fd65f37f23e408",
"invoice.png": "72420749ccc3deaa8025daf7baffcdf79be716b6e8e93ed605effebed01e86c7"
diff --git a/mkdocs.yml b/mkdocs.yml
index dec367b..2f43b1c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -92,6 +92,7 @@ nav:
- Browser PDF generation: guides/browser-pdf.md
- React PDF previews: guides/react-pdf.md
- Vue PDF previews: guides/vue-pdf.md
+ - Queue Node PDF jobs: guides/node-render-queue.md
- Next.js PDF downloads: guides/nextjs-pdf.md
- ASP.NET Core PDF downloads: guides/aspnet-pdf.md
- Variable-data documents: guides/bank-statements.md
diff --git a/tools/build_node_queue.py b/tools/build_node_queue.py
new file mode 100644
index 0000000..1fece51
--- /dev/null
+++ b/tools/build_node_queue.py
@@ -0,0 +1,33 @@
+"""Bundle the Node render-queue example with its pinned public npm package."""
+from hashlib import sha256
+from pathlib import Path
+import json
+import zipfile
+
+ROOT = Path(__file__).resolve().parents[1]
+SOURCE = ROOT / 'examples/node-queue'
+DEST = ROOT / 'docs/assets/node-queue'
+FILES = ['LICENSE', 'README.md', 'invoice.html', 'invoice.css', 'render.mjs',
+ 'package.json', 'package-lock.json']
+record = json.loads((SOURCE / 'source.json').read_text(encoding='utf-8'))
+package = json.loads((SOURCE / 'package.json').read_text(encoding='utf-8'))
+lock = json.loads((SOURCE / 'package-lock.json').read_text(encoding='utf-8'))
+assert record['package_version'] == package['dependencies']['fullbleed']
+assert record['package_integrity'] == lock['packages']['node_modules/fullbleed']['integrity']
+assert record['package_integrity'].startswith('sha512-')
+DEST.mkdir(parents=True, exist_ok=True)
+target = DEST / 'project.zip'
+records = []
+with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
+ for name in FILES:
+ data = (SOURCE / name).read_bytes()
+ info = zipfile.ZipInfo('node-pdf-queue/' + name, date_time=(2026, 10, 7, 0, 0, 0))
+ info.compress_type = zipfile.ZIP_DEFLATED
+ info.create_system = 3
+ info.external_attr = 0o100644 << 16
+ archive.writestr(info, data, compresslevel=9)
+ records.append({'path': name, 'bytes': len(data), 'sha256': sha256(data).hexdigest()})
+record.update(files=records, zip_bytes=target.stat().st_size,
+ zip_sha256=sha256(target.read_bytes()).hexdigest())
+(DEST / 'source.json').write_text(json.dumps(record, indent=2) + '\n', encoding='utf-8', newline='\n')
+print(json.dumps({'ok': True, 'files': len(FILES), 'zip_sha256': record['zip_sha256']}))
diff --git a/tools/verify_node_queue.py b/tools/verify_node_queue.py
new file mode 100644
index 0000000..432fe39
--- /dev/null
+++ b/tools/verify_node_queue.py
@@ -0,0 +1,152 @@
+"""Execute the downloadable queue project and guide using the public npm package."""
+import argparse
+from hashlib import sha256
+import json
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+import zipfile
+
+import pypdf
+import pypdfium2
+
+ROOT = Path(__file__).resolve().parents[1]
+parser = argparse.ArgumentParser(description=__doc__)
+parser.add_argument('--node', default=shutil.which('node'))
+parser.add_argument('--out', type=Path, default=ROOT / 'output/node-queue-verification')
+parser.add_argument('--zip', type=Path)
+parser.add_argument('--update-assets', action='store_true')
+args = parser.parse_args()
+node = Path(args.node).resolve()
+npm = next(p for p in [node.parent / 'node_modules/npm/bin/npm-cli.js',
+ node.parent.parent / 'lib/node_modules/npm/bin/npm-cli.js'] if p.is_file())
+out = args.out.resolve()
+out.mkdir(parents=True, exist_ok=True)
+workspace = Path(tempfile.mkdtemp(prefix='queue project with spaces ', dir=out))
+assets = ROOT / 'docs/assets/node-queue'
+manifest = json.loads((assets / 'source.json').read_text(encoding='utf-8'))
+archive_path = args.zip or assets / 'project.zip'
+digest = lambda data: sha256(data).hexdigest()
+checks = []
+
+
+def check(name, condition):
+ checks.append({'name': name, 'passed': bool(condition)})
+ assert condition, name
+
+
+check('download ZIP matches the source manifest', digest(archive_path.read_bytes()) == manifest['zip_sha256'])
+with zipfile.ZipFile(archive_path) as archive:
+ check('ZIP contains exactly the reviewed files',
+ sorted(archive.namelist()) == sorted('node-pdf-queue/' + f['path'] for f in manifest['files']))
+ for record in manifest['files']:
+ name = record['path']
+ data = archive.read('node-pdf-queue/' + name)
+ destination = (workspace / 'node-pdf-queue' / name).resolve()
+ assert destination.is_relative_to(workspace)
+ check('source and ZIP bytes: ' + name, len(data) == record['bytes'] and digest(data) == record['sha256']
+ and data == (ROOT / 'examples/node-queue' / name).read_bytes())
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ destination.write_bytes(data)
+project = workspace / 'node-pdf-queue'
+env = {**os.environ, 'PATH': str(node.parent) + os.pathsep + os.environ['PATH']}
+commands = []
+
+
+def run(label, command, cwd=project):
+ result = subprocess.run([str(node), *map(str, command)], cwd=cwd, env=env,
+ capture_output=True, text=True, encoding='utf-8', timeout=180)
+ (out / (label + '.stdout.txt')).write_text(result.stdout, encoding='utf-8')
+ (out / (label + '.stderr.txt')).write_text(result.stderr, encoding='utf-8')
+ check(label + ' command succeeds', result.returncode == 0)
+ commands.append({'name': label, 'exit_code': result.returncode})
+ return result.stdout
+
+
+run('install', [npm, 'ci', '--ignore-scripts', '--no-audit', '--no-fund'])
+package = json.loads((project / 'node_modules/fullbleed/package.json').read_text(encoding='utf-8'))
+lock = json.loads((project / 'package-lock.json').read_text(encoding='utf-8'))
+check('installed version and engine match the guide', package['version'] == manifest['package_version']
+ and package['fullbleed']['engineVersion'] == manifest['engine_version'])
+check('lockfile integrity matches the verified release',
+ lock['packages']['node_modules/fullbleed']['integrity'] == manifest['package_integrity'])
+(project / 'observe.mjs').write_text('''import assert from 'node:assert/strict';
+import { writeFile } from 'node:fs/promises';
+let created = 0, peak = 0;
+const active = new Set();
+process.on('worker', worker => {
+ created++; active.add(worker); peak = Math.max(peak, active.size);
+ worker.once('exit', () => active.delete(worker));
+});
+assert.equal(import.meta.resolve('fullbleed'), new URL('node_modules/fullbleed/src/index.js', import.meta.url).href);
+await import('./render.mjs');
+assert.equal(created, 6); assert.equal(peak, 2); assert.equal(active.size, 0);
+await writeFile('workers.json', JSON.stringify({ created, peak, remaining: active.size,
+ node: process.version, platform: process.platform }));
+''', encoding='utf-8')
+batch = json.loads(run('batch', ['observe.mjs']))
+workers = json.loads((project / 'workers.json').read_text(encoding='utf-8'))
+check('six distinct invoices rendered without missing glyphs', len(batch['invoices']) == 6
+ and len({row['reference'] for row in batch['invoices']}) == 6
+ and all(row['pages'] == 1 and row['missingGlyphs'] == 0 for row in batch['invoices']))
+check('actual worker concurrency stays at two and all workers exit',
+ workers['created'] == 6 and workers['peak'] == 2 and workers['remaining'] == 0)
+
+
+def inspect(path, expected):
+ pdf = pypdf.PdfReader(path)
+ check(path.name + ': one PDF page', len(pdf.pages) == 1)
+ texts = {'pypdf': pdf.pages[0].extract_text()}
+ with pypdfium2.PdfDocument(path) as document:
+ assert len(document) == 1
+ page = document[0]
+ textpage = page.get_textpage()
+ try:
+ texts['PDFium'] = textpage.get_text_range()
+ finally:
+ textpage.close()
+ page.close()
+ for reader, text in texts.items():
+ normalized = ' '.join(text.split())
+ check(path.name + ': expected fields in ' + reader, all(value in normalized for value in expected))
+
+
+rendered = project / 'output/queued-invoices'
+outputs = {}
+for row in batch['invoices']:
+ name = row['reference'] + '.pdf'
+ inspect(rendered / name, [row['reference'], row['customer'], '1,870.00'])
+for name, expected in manifest['rendered'].items():
+ data = (rendered / name).read_bytes()
+ outputs[name] = digest(data)
+ check(name + ': matches reviewed output', outputs[name] == expected)
+ if args.update_assets:
+ (assets / name).write_bytes(data)
+ else:
+ check(name + ': published asset bytes match', data == (assets / name).read_bytes())
+run('replay', ['render.mjs'])
+check('exact-input replay preserves every PDF and PNG',
+ all(digest((rendered / name).read_bytes()) == value for name, value in outputs.items()))
+
+guide = (ROOT / 'docs/guides/node-render-queue.md').read_text(encoding='utf-8')
+snippets = re.findall(r'```javascript\n(.*?)```', guide, re.S)
+check('guide contains one executable JavaScript example', len(snippets) == 1)
+snippet_dir = project / 'guide-snippet'
+snippet_dir.mkdir()
+(snippet_dir / 'guide.mjs').write_text(snippets[0], encoding='utf-8')
+run('guide-snippet', ['guide.mjs'], snippet_dir)
+for reference in [1042, 1043, 1044]:
+ inspect(snippet_dir / f'invoice-{reference}.pdf', [f'NS-{reference}', '1,200.00'])
+
+report = {'schema': 'fullbleed.node-queue-example-verification.v1', 'ok': True,
+ 'package_version': package['version'], 'engine_version': package['fullbleed']['engineVersion'],
+ 'workers': workers, 'readers': ['pypdf', 'PDFium'], 'commands': commands,
+ 'project_zip_sha256': manifest['zip_sha256'], 'outputs': outputs, 'checks': checks,
+ 'scope': 'This downloaded batch, guide snippet, and exact replay; no throughput or memory-capacity claim.'}
+(out / 'verification.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8', newline='\n')
+if args.update_assets:
+ (assets / 'verification.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8', newline='\n')
+print(json.dumps({'ok': True, 'checks': len(checks), 'workers': workers}))