ArrayBuffer Detach: Why Reprocess Threw Empty PDF on Every Call
TLDR
Invoking PDF.js parsing methods (pdfjsLib.getDocument({ data: bytes })) causes zero-copy ArrayBuffer transfers to internal Web Worker threads, immediately detaching the array buffer in the calling context. Storing a direct reference to bytes causes subsequent single-page reprocessing requests (triggered from region editors) to fail with "Empty PDF" errors due to byteLength === 0. Executing bytes.slice() prior to transfer guarantees data availability.
| Caching Strategy | Pointer Type | Post-Transfer byteLength | Reprocessing Call Status |
|---|---|---|---|
| Direct Reference | _cachedBytes = bytes | 0 (Detached / Neutered) | Fails ("Empty PDF" error) |
| Sliced Buffer Copy | _cachedBytes = bytes.slice() | Original Byte Length | 100% Deterministic Reprocessing |
Problem statement: the region editor reprocess crash
When users edited bounding boxes on the region editor canvas and clicked "Reprocess Page", the worker thread crashed or threw an unhandled internal exception: Error: Empty PDF.
Debugging revealed that while initial document loading succeeded, _cachedBytes.byteLength evaluated to 0 inside the worker thread prior to reprocessing.
Technical failure mode: ArrayBuffer transfer semantics
Passing Uint8Array data into PDF.js triggers structured clone transfer semantics:
// DEFECTIVE IMPLEMENTATION: Storing reference prior to transfer
let _cachedBytes = null;
self.onmessage = async (e) => { const { bytes } = e.data; // Storing direct reference to input Uint8Array _cachedBytes = bytes;
// getDocument transfers bytes.buffer, setting byteLength to 0! const pdf = await pdfjsLib.getDocument({ data: bytes }).promise; };
When the region editor subsequently triggered a reprocess pass:
// FAILS: _cachedBytes.byteLength is 0!
const pdf = await pdfjsLib.getDocument({ data: _cachedBytes }).promise;
PDF.js received an empty 0-byte buffer and threw an internal parsing exception.
The fix: pre-transfer independent buffer cloning
Copy the byte array using .slice() before passing the original buffer to PDF.js:
// REFACTORED WORKER: Slice array before transfer neuters buffer
let _cachedBytes = null;
self.onmessage = async (e) => { const { bytes } = e.data;
// 1. Create independent ArrayBuffer copy BEFORE transfer _cachedBytes = bytes.slice();
// 2. Transfer original bytes to PDF.js const pdf = await pdfjsLib.getDocument({ data: bytes }).promise; };
Safety guard implementation
Validate cached buffer integrity before executing reprocessing passes:export async function executeRegionReprocess(pageNum, overrides) {
if (!_cachedBytes || _cachedBytes.byteLength === 0) {
throw new Error('Cached PDF byte buffer is neutered or empty. Re-initialize document.');
}
// Safe to parse cached bytes const pdf = await pdfjsLib.getDocument({ data: _cachedBytes }).promise; const page = await pdf.getPage(pageNum); return reprocessPageWithOverrides(page, overrides); }
Rule of thumb: Always clone typed byte arrays (bytes.slice()) before passing them to libraries that execute zero-copy Web Worker transfers.