PDF.js Source Walkthrough: pdf_manager
October 27, 2026 · StreamPDF Team

Introduction
src/core/pdf_manager.js is only 244 lines long, but it decides where the bytes come from inside the worker. PDFDocument, XRef, Parser and Catalog all recognize exactly one thing: a Stream that can be read synchronously. The file may come from an in-memory ArrayBuffer, or from network responses fetched by range; the manager's job is to flatten both sources into the same Stream, then give the layers above a uniform asynchronous access interface.
The abstraction solves a structural tension:
| Layer | Desired reads | Reality |
|---|---|---|
XRef / Parser | Synchronous, random access | Network chunks may not have arrived |
| Network transport | Asynchronous, on demand | Only one slice at a time, with latency |
PDFDocument | Does not care about the source | Must support both in-memory and streaming |
BasePdfManager wraps missing bytes as MissingDataException, wraps filling them as requestRange, and joins the two with ensure. What the layer above sees is still an ordinary property read or method call; retrying, waiting and chunking all happen inside the manager.
This article follows the real implementation of the current version, including one place where the interface differs from older versions: the file no longer forwards members such as numPages, pdfInfo and getData, and the abstract interface has shrunk to the few methods that genuinely need polymorphism.
Module Overview
The file has three parts: a URL helper, a base class and two subclasses.
| Element | Category | Responsibility |
|---|---|---|
parseDocBaseUrl | Module function | Validate and normalize the document base URL |
BasePdfManager | Abstract base class | Hold options, provide shared delegation, declare the methods that must be implemented |
LocalPdfManager | Subclass | Holds an in-memory Stream directly |
NetworkPdfManager | Subclass | Holds a ChunkedStreamManager and a ChunkedStream |
The base class members split into four groups by who implements them:
| Group | Members | Notes |
|---|---|---|
| Read-only properties | docId, password, docBaseUrl | Document identity and security information |
| Shared delegation | ensureDoc, ensureCatalog, initDocument, getPage, fontFallback, cleanup | Forwarded straight to PDFDocument or Catalog |
| Concrete implementation | updatePassword | Updates the password and syncs it to the encryptor |
| Abstract methods | ensure, requestRange, requestLoadedStream, sendProgressiveData, terminate | Subclasses must override them, otherwise unreachable fires |
The difference between the two subclasses fits in one table:
| Dimension | LocalPdfManager | NetworkPdfManager |
|---|---|---|
| Data source | A complete byte array | Range requests or progressive chunks |
| Underlying stream | Stream | ChunkedStream |
ensure with missing bytes | Cannot happen | Catches the exception, requests the range, retries |
requestRange | Not implemented | Delegates to ChunkedStreamManager |
requestLoadedStream | Returns an already resolved promise | requestAllChunks |
sendProgressiveData | Not implemented | onReceiveData |
terminate | Empty | Aborts the chunk manager |
The worker picks one in getPdfManager based on transport capabilities:
| Condition | Choice | Stream type |
|---|---|---|
The caller passed complete data | LocalPdfManager | Stream |
| Response headers allow range requests | NetworkPdfManager | ChunkedStream |
| No range support, read while downloading | Buffer first, then LocalPdfManager after the download | Stream |
| Range support and a streaming download in progress | NetworkPdfManager with sendProgressiveData | ChunkedStream |
parseDocBaseUrl
docBaseUrl is the base for resolving relative URIs inside the document. It comes from the options the caller passes, but it has to pass validation before it reaches the document object.
function parseDocBaseUrl(url) {
if (url) {
const absoluteUrl = createValidAbsoluteUrl(url);
if (absoluteUrl) {
return absoluteUrl.href;
}
warn(`Invalid absolute docBaseUrl: "${url}".`);
}
return null;
}The behaviour reduces to four kinds of input:
| Input | Handling | Result |
|---|---|---|
| Null or an empty string | Return immediately | null |
| A valid absolute URL | createValidAbsoluteUrl parses it and validates the scheme | The normalized href |
| A relative path | No base to resolve against, parsing fails | Warning, null |
| A disallowed scheme | Scheme validation fails | Warning, null |
It accepts absolute URLs only, and never joins a relative path onto the current page address. That is deliberate: the worker runs on its own thread with no document.baseURI, so resolving relative paths stays the main thread's responsibility, completed by the caller before loading starts.
An invalid value does not throw; it logs a warning and degrades to null. The document still opens, and relative URIs encountered later simply cannot be resolved. For a purely presentational option, that degradation is more reasonable than aborting the load.
The value ultimately appears on Catalog.baseUrl:
| Priority | Source |
|---|---|
| 1 | /Base in the catalog dictionary's /URI, and it must be a valid absolute URL |
| 2 | pdfManager.docBaseUrl |
| 3 | null |
Catalog uses this base to resolve relative links in action dictionaries, attachments and annotations; the annotation layer receives the same baseUrl. So parseDocBaseUrl, ten lines long, is the starting point of the whole URL resolution chain.
Design note: External input is normalized before it enters the core layer.
docBaseUrlis parsed once, at manager construction; every later reader gets either a validatedhrefornull, and no one has to re-check validity before each use.
Construction: One Base Class, Two Implementations
The constructor
The base constructor does three things: store the shared options, run the feature checks once, and distribute the configuration to the module-level option containers.
constructor({
// source,
// disableAutoFetch,
docBaseUrl,
docId,
enableXfa,
evaluatorOptions,
handler,
// length,
password,
// rangeChunkSize,
}) {
if (
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
this.constructor === BasePdfManager
) {
unreachable("Cannot initialize BasePdfManager.");
}
this._docBaseUrl = parseDocBaseUrl(docBaseUrl);
this._docId = docId;
this._password = password;
this.enableXfa = enableXfa;The commented-out source, disableAutoFetch, length and rangeChunkSize parameters belong to the network implementation only, and the base class deliberately does not accept them. Declaring only the fields the base class itself uses lets both subclasses share one argument object without the base class knowing anything about transport details.
The instantiation guard only takes effect in test builds. In production builds PDFJSDev is replaced by a constant and the check is statically eliminated; in tests, an accidental new BasePdfManager(...) fails immediately with "cannot initialize the base class".
docId participates in global ID generation. PDFDocument has a global ID factory that uses g_${docId} as the document prefix, appends a font index and produces a font identity shared across pages. That identity travels all the way to the main thread and is used to reuse an already created FontFace.
password is the initial password, and may later be replaced by updatePassword. It plays no part in stream selection and affects only XRef decryption.
// Check `OffscreenCanvas` and `ImageDecoder` support once,
// rather than repeatedly throughout the worker-thread code.
evaluatorOptions.isOffscreenCanvasSupported &&= FeatureTest.isOffscreenCanvasSupported;
evaluatorOptions.isImageDecoderSupported &&= FeatureTest.isImageDecoderSupported;
this.evaluatorOptions = Object.freeze(evaluatorOptions);The two &&= operators combine caller intent with runtime capability:
| Option | Combined with | Effect |
|---|---|---|
isOffscreenCanvasSupported | FeatureTest.isOffscreenCanvasSupported | Enabled only when both are true |
isImageDecoderSupported | FeatureTest.isImageDecoderSupported | Enabled only when both are true |
This code modifies the object it was passed. The evaluatorOptions the caller hands in are mutable configuration until they reach the manager; inside, they are patched in place and immediately frozen with Object.freeze. The freeze is shallow: top-level properties can no longer change, but nested objects remain mutable. The real constraint is by convention: later code only reads.
The options are then distributed to eight module-level containers:
| Module | Options received | Purpose |
|---|---|---|
ImageResizer | evaluatorOptions | Image scaling thresholds |
JpegStream | evaluatorOptions | JPEG decoding parameters |
OperatorList | evaluatorOptions | Operator list caching strategy |
IccColorSpace | Options plus handler | Reads wasmUrl, useWasm, useWorkerFetch |
CmykICCBasedCS | Options plus handler | Reads iccUrl |
PDFFunctionFactory | Options plus handler | Reads useWasm |
Pattern | Options plus handler | Reads hasGPU |
WasmImage | Options plus handler | WASM image decoding |
The last five receive the option object with handler, but only WasmImage really uses it: it needs cross-thread messaging to load a WASM module inside the worker. The others only read their own URL and switch fields, and an extra handler does no harm.
These setOptions calls are module-level static methods and should run only once per worker. PDF.js's default deployment is one worker per document, so documents cannot overwrite each other's options; if a single worker opened several documents, the later options would overwrite the earlier ones, which is a known boundary of these static containers.
Design note: Feature detection runs once, options are injected once, and then the object is frozen. Inside the worker PDF.js deliberately avoids checking capabilities on every parse, compressing those global decisions into the document construction phase.
Delegation: ensureDoc and ensureCatalog
The base class does not expose pdfDocument details directly; it offers two clearly named entries, one for document properties and one for catalog properties.
ensureDoc
ensureDoc(prop, args) {
return this.ensure(this.pdfDocument, prop, args);
}
ensureCatalog(prop, args) {
return this.ensure(this.pdfDocument.catalog, prop, args);
}Both methods are one-line forwards; only the target object differs. ensureDoc("numPages") reads a getter on PDFDocument; ensureCatalog("numPages") reads a getter on Catalog. The same name may exist on both objects with different meanings: the document layer considers linearization and XFA, while the catalog layer only knows the page tree.
this.pdfDocument.catalog inside ensureCatalog is a plain property access. Catalog is created by parse, and initDocument guarantees that parse has already finished, so the catalog object is guaranteed to exist here; what can really be missing is the bytes for later method calls, and those happen inside the try block of ensure.
Argument passing is uniform too: if the property value is a function, args is passed as its argument array; otherwise args is ignored and the property value is returned.
initDocument
The five document initialization steps all run through ensureDoc:
async initDocument(recoveryMode) {
await this.ensureDoc("checkHeader");
await this.ensureDoc("parseStartXRef");
await this.ensureDoc("parse", [recoveryMode]);
// Check that at least the first page can be successfully loaded,
// since otherwise the XRef table is definitely not valid.
await this.ensureDoc("checkFirstPage", [recoveryMode]);
// Check that the last page can be successfully loaded, to ensure that
// `numPages` is correct, and fallback to walking the entire /Pages-tree.
await this.ensureDoc("checkLastPage", [recoveryMode]);
}The order is not arbitrary:
| Step | Purpose | Failure means |
|---|---|---|
checkHeader | Validate the file header and version | Not a valid PDF |
parseStartXRef | Locate the start of the cross-reference table | The trailing structure is damaged |
parse | Build the XRef, handle object streams and encryption | The cross-reference is unusable |
checkFirstPage | At least the first page can be retrieved | The page tree root is unusable |
checkLastPage | Verify the page count, falling back to walking the whole page tree if needed | The page count declaration is wrong |
Every step can fail because of missing bytes, and every step can change internal state. ensure retries inside a single step and never restarts initialization from the beginning; only an exception other than a missing-data one interrupts it.
recoveryMode is passed down to parse, checkFirstPage and checkLastPage. The worker initializes with false, and if it gets an XRefParseException it waits for requestLoadedStream to finish and tries again with true. The second pass has the complete bytes and can repair more aggressively, for example rebuilding the cross-reference object by object.
The Abstract Methods
Five methods in the base class call unreachable directly; they are the contract subclasses must fulfil:
| Method | Why a subclass must implement it | LocalPdfManager | NetworkPdfManager |
|---|---|---|---|
ensure | Decides whether to wait and retry on missing bytes | Reads properties directly | Catches and requests the range |
requestRange | Decides how a range becomes a network request | Never called | Delegates to the chunk manager |
requestLoadedStream | Decides how to get the complete bytes | An already resolved promise | Waits for all chunks |
sendProgressiveData | Decides how progressively arriving chunks are handled | Never called | Writes into the chunked stream |
terminate | Decides how to abort a transfer in progress | Empty | Aborts the chunk manager |
unreachable throws a clear error in test builds and is optimized into an unreachable marker in production builds. It is safer than returning undefined: if a subclass forgets to implement something, the call site fails immediately instead of continuing with an empty value.
updatePassword is the only action-shaped method with a concrete implementation in the base class:
updatePassword(password) {
this._password = password;
this.pdfDocument.xref.encrypt?.setPassword(password);
}It updates two places at once: the manager's own _password for later reads, and the encryptor on XRef. Optional chaining is used because an unencrypted document has no encrypt, and then only the field needs updating.
cleanup, getPage and fontFallback are pure forwards too; they do not involve the byte source, so subclasses need not repeat them.
Design note: The base class abstracts only the methods related to the data source. Behaviour unrelated to transport, such as page retrieval, font fallback and cache cleanup, is implemented once through delegation, so the two subclasses do not each carry an identical copy.
LocalPdfManager: Everything in Memory
The ensure method
class LocalPdfManager extends BasePdfManager {
constructor(args) {
super(args);
const stream = new Stream(args.source);
this.pdfDocument = new PDFDocument(this, stream);
this._loadedStreamPromise = Promise.resolve(stream);
}
async ensure(obj, prop, args) {
const value = obj[prop];
if (typeof value === 'function') {
return value.apply(obj, args);
}
return value;
}
requestLoadedStream(noFetch = false) {
return this._loadedStreamPromise;
}
terminate(reason) {}
}The implementation is so short it barely looks like a manager, because there is nothing asynchronous in the in-memory case:
args.sourceis a complete byte array, andnew Streamwraps it into a randomly accessible stream.PDFDocumentreceives the manager and the stream at construction, and all later parsing runs synchronously over that memory._loadedStreamPromiseresolves immediately, so no caller waiting for complete bytes ever blocks.
ensure has no try/catch because Stream never throws MissingDataException. An out-of-range read throws a range error, which is genuine document corruption and should be visible to the layer above rather than retried.
One easily missed detail: an async function flattens its return value anyway. If the property value is itself a promise, for example numPages on an XFA document, return value still gives the caller the promise's final value. That is why ensure is declared async even though the in-memory implementation awaits nothing.
terminate is empty. There is no network request to abort and no chunk manager to close; when the instance is discarded the memory is released by the garbage collector.
| Characteristic | In-memory behaviour |
|---|---|
| First-read latency | None |
| Missing-byte exceptions | Never happen |
| Retrieving complete bytes | Immediate |
| Abort cost | None |
| Supported document size | Limited by memory |
NetworkPdfManager: Filling Chunks on Demand
The ensure method
class NetworkPdfManager extends BasePdfManager {
constructor(args) {
super(args);
this.streamManager = new ChunkedStreamManager(args.source, {
msgHandler: args.handler,
length: args.length,
disableAutoFetch: args.disableAutoFetch,
rangeChunkSize: args.rangeChunkSize,
});
this.pdfDocument = new PDFDocument(this, this.streamManager.getStream());
}
async ensure(obj, prop, args) {
try {
const value = obj[prop];
if (typeof value === "function") {
return await value.apply(obj, args);
}
return value;
} catch (ex) {
if (!(ex instanceof MissingDataException)) {
throw ex;
}
await this.requestRange(ex.begin, ex.end);
return this.ensure(obj, prop, args);
}
}The constructor hands four network parameters to ChunkedStreamManager: the message handler, the total file length, whether auto-prefetch is disabled, and the chunk size. The ChunkedStream returned by getStream() is a fixed-length array where loaded chunks are written in and unloaded chunks stay blank.
ensure is the most critical piece in the file. It catches exactly one exception type: MissingDataException.
| Exception type | Handling | Reason |
|---|---|---|
MissingDataException | Request the missing range, retry recursively | The data has simply not arrived yet, so waiting is fine |
| Any other exception | Rethrow as-is | Corrupt document, failed decryption, syntax error |
The retry path requests the range and then re-runs the same property read or method call. This means a getter wrapped by ensure must be able to use existing caches on re-entry. PDF.js relies heavily on shadow caches: the first successful computation writes the result back to the instance property, the second read hits it directly, and nothing is parsed twice. If a method restarted from scratch on every call, retries would do the same useless work over and over.
The recursion has no attempt limit. Safety comes from the other side: the promise returned by requestRange only resolves once the corresponding chunk really arrives, and if the manager is aborted it rejects, so the recursion ends naturally. As long as the requested range really covers the gap, every round makes progress.
Seen next to ObjectLoader, the division is clear:
| Scenario | Who handles it | Request granularity |
|---|---|---|
| Document initialization, single property reads | NetworkPdfManager.ensure | One missing range |
| Page resource batch prefetch | ObjectLoader | Many ranges merged |
ensure is the backstop that guarantees any synchronous access eventually gets its bytes; ObjectLoader is the optimization that merges many small gaps into a few requests so that the backstop is rarely triggered.
requestRange and requestLoadedStream
requestRange(begin, end) {
return this.streamManager.requestRange(begin, end);
}
requestLoadedStream(noFetch = false) {
return this.streamManager.requestAllChunks(noFetch);
}requestRange only forwards. How a range turns into chunk numbers, how chunks are merged, and how GetRangeReader is sent to the main thread through the message handler are all decided by ChunkedStreamManager. The manager cares about none of that; it only needs a promise for this range to arrive.
The noFetch parameter of requestLoadedStream deserves a separate note:
noFetch | Behaviour | Used by |
|---|---|---|
false | Request every missing chunk and wait for all of them | GetData, SaveDocument |
true | Send no request, only wait for transfers already scheduled | The DataLoaded event, recovery mode |
In the worker, GetData waits for this promise and then returns stream.bytes; SaveDocument waits for it first as well, because writing back needs the complete original bytes. noFetch set to true serves the case of confirming that the current download has finished, so a status notification does not trigger another request round.
sendProgressiveData and terminate
sendProgressiveData(chunk) {
this.streamManager.onReceiveData({ chunk });
}
terminate(reason) {
this.streamManager.abort(reason);
}Progressive data is the entry point of read-while-downloading. In getPdfManager, the worker reads the network stream and hands each chunk to sendProgressiveData; if the manager is not built yet, the chunks go into a temporary array and are replayed afterwards. The object onReceiveData receives carries only chunk, no begin, because a progressive download always continues from the current write pointer and needs no explicit offset.
terminate fails all pending range requests. When the worker terminates it also calls a separate cancel function to abort the underlying XHR, but this layer only has to guarantee that promises being awaited do not hang forever.
Comparing the two implementations side by side makes the real boundary of the abstraction clear:
| Behaviour | In-memory implementation | Network implementation |
|---|---|---|
| Reading a property | Synchronous read | Synchronous read, waits when bytes are missing |
| Reading a method | Direct call | Direct call, retries when bytes are missing |
| Waiting for complete bytes | Already ready | Requests the missing chunks |
| Receiving progressive data | Not applicable | Writes into the chunked stream |
| Abort | No-op | Aborts the chunk manager and rejects pending requests |
Design note: The abstract base class specifies what can be done, not how.
ensurediffers a lot between the two subclasses, yet is fully transparent to callers; that is precisely whyPDFDocumentcan hold one set of parsing code and still support both in-memory and streaming sources.
Document, XRef and the Manager
The manager parses no bytes itself; it only hands out the stream. PDFDocument stores the manager and the stream at construction and creates XRef immediately:
this.pdfManager = pdfManager;
this.stream = stream;
this.xref = new XRef(stream, pdfManager);The XRef constructor stores the same two things:
constructor(stream, pdfManager) {
this.stream = stream;
this.pdfManager = pdfManager;Together they form a fixed reference chain: PDFDocument holds xref and stream, and XRef holds stream and pdfManager. Whenever any layer needs bytes, it can follow pdfManager back to ensure or requestRange without extra global state.
Different readers go through different entries:
| Reader | What it actually reads | Path when bytes are missing |
|---|---|---|
PDFDocument.checkHeader | stream.getBytes | ensureDoc catches and retries |
PDFDocument.startXRef | stream.getBytes | ensureDoc catches and retries |
XRef.parse | stream and Parser | ensureDoc("parse") |
XRef.fetch | stream.getBytes | Caught by an outer ensure |
ObjectLoader | xref.stream.manager.requestRanges | Batches requests itself, bypassing ensure |
PDFDocument.#getByteRange | stream.getByteRange | Calls requestRange directly |
Catalog properties | Reads XRef indirectly | ensureCatalog catches and retries |
In the in-memory implementation all these paths degrade to plain array reads. In the network implementation a typical read goes through six steps:
- The layer above calls
ensureDoc("parse"), entering the try block ofNetworkPdfManager.ensure. XRef.parsereads the stream,ChunkedStreamfinds the target chunk not loaded, and throws an exception carryingbeginandend.ensurecatches it, callsrequestRange(begin, end)and waits.ChunkedStreamManagerconverts the range into chunks and sends a range request to the main thread through the message handler.- The data is written into the chunked stream when it returns, and the corresponding promise resolves.
ensurerecursively re-runsensureDoc("parse"); this time the read hits loaded chunks, and the result of the single getter is cached byshadow, so later access never takes this path again.
PDFDocument.#getByteRange is the exception. It exists for signature validation and reads only a short contiguous stretch; rather than throwing up to ensureDoc, it catches locally and calls requestRange directly, saving one level of re-entry:
async #getByteRange(begin, end) {
try {
return this.stream.getByteRange(begin, end);
} catch (ex) {
if (!(ex instanceof MissingDataException)) {
throw ex;
}
await this.pdfManager.requestRange(begin, end);
return this.#getByteRange(begin, end);
}
}This code has almost the same shape as NetworkPdfManager.ensure, which shows that catching missing bytes, requesting the range and retrying is the common pattern of this layer. Only the retried object differs: one re-runs a property read, the other a byte slice.
ensure has no retry count and does not need one. It relies on a stronger guarantee: the requested range covers the gap named in the exception, and once the range has arrived the same read must be able to advance. If the underlying transfer is aborted, the promise rejects and the recursion stops. Leaving the termination condition to the data layer is more reliable than guessing an upper bound in the retry layer.
Design note: Retry logic for byte reads is allowed in exactly two places:
ensure, and the very few local methods that need precise slices. The more call sites there are, the greater the risk of duplicate requests and inconsistent state; once the entry points are narrowed,XRefandCatalogcan stay purely synchronous.
Version Note: The Interface Boundary
Members of BasePdfManager are not identical across PDF.js versions. This version removed some forwarding getters so the manager can focus on supplying data. If the material you read mentions the names below, here is where they live in this version:
| Name | Present in this version | Actual location |
|---|---|---|
pdfDocument | Yes | Instance property assigned in the subclass constructor |
docId | Yes | Base class getter |
numPages | No | PDFDocument.numPages, reached through ensureDoc |
numPagesWithOffset | No | Not present in this version |
pdfInfo | No | The payload of the GetDoc message, holding numPages, fingerprints and htmlForXfa |
ensure | Yes | Base class abstract method |
ensureDoc | Yes | Base class delegation method |
ensureCatalog | Yes | Base class delegation method |
requestRange | Yes | Base class abstract method, delegated to the chunk manager in the network implementation |
getData | No | requestLoadedStream; the worker's GetData takes stream.bytes |
requestRanges | No | ChunkedStreamManager.requestRanges, called by ObjectLoader |
cleanup | Yes | Base class delegates to PDFDocument.cleanup |
terminate | Yes | Base class abstract method |
updateMetadata | No | Not present in this version |
updateDocument | No | Saving goes through SaveDocument and the writer |
These differences show one thing: the abstract interface shrinks as the implementation evolves. Early versions put getters such as numPages and pdfInfo on the manager, which saved callers one level of ensureDoc, but the manager ended up knowing too much about the document's internals. Now those reads all go through ensureDoc, and the manager keeps only the polymorphic points that are tightly tied to transport.
When reading source, trust the current file over a remembered member list from some version. To decide whether a member belongs to the abstract interface, check whether it is marked with unreachable in several places, or whether both subclasses implement it differently.
Conclusion
pdf_manager.js is PDF.js's data supply layer: upward it gives PDFDocument and XRef a uniform stream and access interface, and downward it hides the differences between in-memory bytes, range requests and progressive downloads.
Where It Sits in the Overall Chain
| Stage | Module | Input | Output |
|---|---|---|---|
| Starting a load | display/api.js | File, URL, range parameters | GetDocRequest |
| Choosing the implementation | getPdfManager in worker.js | Complete data or range capability | LocalPdfManager or NetworkPdfManager |
| Creating the stream | Manager constructor | Byte array or network stream | Stream or ChunkedStream |
| Creating the document | PDFDocument | Manager and stream | pdfDocument |
| Initialization | initDocument | Recovery mode | A parsed XRef and Catalog |
| Property access | ensureDoc / ensureCatalog | Property name and arguments | A value, or one retry |
| Filling bytes | requestRange / requestLoadedStream | A range or everything | Data arriving |
| Lifecycle | cleanup / terminate | The trigger reason | Cache release and request abort |
In one sentence: the manager keeps the possibility of missing data behind closed doors, so PDFDocument and XRef can assume the bytes are always available.
Five Design Principles
- Dependency inversion.
PDFDocumentdepends only on the manager's abstract interface, not on whether the data comes from memory or the network; transport differences are confined to the two subclasses. - Missing bytes as control flow.
MissingDataExceptionis not an error but a signal asking for more data; only it is caught byensureand retried. - Options injected once. Feature detection, option freezing and module-level
setOptionsall happen during document construction, and the hot path repeats none of those checks. - Two implementations, one semantics. Whether in memory or over the network,
ensurepromises to return the property value or throw a real error; the in-memory implementation merely reduces waiting to zero cost. - Explicit lifecycle. Complete bytes are obtained through
requestLoadedStream, caches are released throughcleanup, and transfers are aborted throughterminate; every action has a clear entry point and none relies on implicit collection.
StreamPDF design principle: anything that can be handled ahead of time is absolutely not deferred to parse time; the server solves all performance problems once. The browser only fetches and renders the data it must.
Related articles
- PDF.js Source Walkthrough: object_loaderOctober 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_coreOctober 27, 2026
- PDF.js Source Walkthrough: cryptoOctober 27, 2026
- PDF.js Source Walkthrough: core_utilsOctober 27, 2026
- PDF.js Source Walkthrough: primitivesOctober 27, 2026
- PDF.js Source Walkthrough: worker_streamOctober 27, 2026
- PDF.js Source Walkthrough: ccitt_streamOctober 27, 2026
- PDF.js Source Walkthrough: jpx_streamOctober 27, 2026
- PDF.js Source Walkthrough: jbig2_streamOctober 27, 2026
- PDF.js Source Walkthrough: jpeg_streamOctober 27, 2026
- PDF.js Source Walkthrough: decrypt_streamOctober 27, 2026
- PDF.js Source Walkthrough: brotli_streamOctober 27, 2026
- PDF.js Source Walkthrough: run_length_streamOctober 27, 2026
- PDF.js Source Walkthrough: ascii_hex_streamOctober 27, 2026
- PDF.js Source Walkthrough: ascii_85_streamOctober 27, 2026
- PDF.js Source Walkthrough: lzw_streamOctober 27, 2026
- PDF.js Source Walkthrough: predictor_streamOctober 27, 2026
- PDF.js Source Walkthrough: flate_streamOctober 27, 2026
- PDF.js Source Walkthrough: decode_streamOctober 27, 2026
- PDF.js Source Walkthrough: chunked_streamOctober 27, 2026
- PDF.js Source Walkthrough: streamOctober 27, 2026
- PDF.js Source Walkthrough: base_streamOctober 27, 2026
- PDF.js Deep Dive 11: ParserOctober 26, 2026
- PDF.js Deep Dive 10: XRefOctober 26, 2026
- PDF.js Deep Dive 9: CanvasGraphicsOctober 26, 2026
- PDF.js Deep Dive 8: PDFDocumentOctober 26, 2026
- The AI Era: What Should We Do to Survive? (Reflections on Founding StreamPDF)October 26, 2026
- PDF.js Deep Dive 7: PartialEvaluatorOctober 25, 2026
- PDF.js Deep Dive 6: PDFManager.initDocumentOctober 25, 2026
- PDF.js Deep Dive 5: PDFFetchStream and PDFWorkerStreamOctober 25, 2026
- PDF.js Deep Dive 4: MessageHandlerOctober 22, 2026
- PDF.js Deep Dive 3: getViewport & renderOctober 22, 2026
- PDF.js Deep Dive 2: getDocument & getPageOctober 19, 2026
- PDF.js Deep Dive 1: The Architecture of PDF.jsOctober 16, 2026