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

Introduction
This installment examines src/shared/internal_evt.js, a seven-line implementation after its license and synchronization comment. A mirrored file exists at web/internal_evt.js.
The module is not an event bus and defines no event names. It exports two values used to classify event listeners:
INTERNAL_EVT, a token selected during preprocessing;internalOpt, a frozen options object containing that token.
The actual registration and dispatch logic lives in web/event_utils.js. Reading the two sides together reveals the real mechanism: internal listeners authenticate their classification with a build-specific token, and EventBus delivers them before external listeners for the same event.
Module Overview
| Block | Lines | Responsibility |
|---|---|---|
| Synchronization comment | 16-18 | Require this file and web/internal_evt.js to remain aligned |
INTERNAL_EVT | 20-23 | Select the injected build token or the source/testing fallback |
internalOpt | 25 | Package the token as an immutable listener option |
| Exports | 27 | Make the token available to EventBus and the options object available to internal callers |
The design separates roles cleanly: this module owns the capability value, internal components opt in with internalOpt, and EventBus performs the strict identity comparison that determines listener classification.
INTERNAL_EVT: Selecting the Token at Build Time
const INTERNAL_EVT =
typeof PDFJSDev !== 'undefined' && !PDFJSDev.test('TESTING')
? PDFJSDev.eval('INTERNAL_EVT')
: 'internalEvent';There are two modes:
| Mode | Selected value | Reason |
|---|---|---|
| Preprocessed, non-testing build | PDFJSDev.eval("INTERNAL_EVT") | Inject the value supplied by the build definitions |
| Raw source or testing build | "internalEvent" | Provide a deterministic fallback without requiring production injection |
The production value comes from the DEFINES object in gulpfile.mjs:
const DEFINES = Object.freeze({
// Other build flags...
INTERNAL_EVT: crypto.randomUUID(),
});The Babel preprocessor evaluates PDFJSDev.eval against those definitions and replaces the expression with the resulting literal. Consequently, the shipped module does not call crypto.randomUUID() at runtime; the UUID is chosen when the build is produced and remains fixed within that build.
This token is not the name of an internal event. It is a capability-like value used in listener options. A caller cannot become internal merely by passing { internal: true }, because EventBus compares the supplied value with INTERNAL_EVT using strict equality.
The fixed testing fallback serves a different purpose. Source-level tests and unprocessed imports need both sides of the comparison to agree without depending on a production preprocessing pass. It should not be confused with the randomized value used for non-testing builds.
internalOpt: Freezing the Internal Registration Option
const internalOpt = Object.freeze({ internal: INTERNAL_EVT });internalOpt turns the token into the exact option shape expected by EventBus.on. Freezing prevents one consumer from changing the shared object's internal property and silently affecting later registrations. The freeze is shallow, but the object has only one primitive-valued property, so that is sufficient here.
Internal callers can pass the object directly or combine it with other standard options. For example, the annotation editor UI manager registers several listeners with an abort signal:
const evtOpts = { signal, ...internalOpt };
eventBus.on('editingaction', this.onEditingAction.bind(this), evtOpts);
eventBus.on('pagechanging', this.onPageChanging.bind(this), evtOpts);
eventBus.on('scalechanging', this.onScaleChanging.bind(this), evtOpts);The spread copies the token into a fresh options object while preserving signal. Other call sites add once in the same way. This keeps lifecycle and delivery-classification concerns composable instead of inventing a separate registration API for every combination.
EventBus.on() and dispatch(): Enforcing Internal-First Delivery
The token becomes meaningful when EventBus records a listener. In web/event_utils.js, registration stores a boolean derived from identity with the private module contract:
this.#listeners.getOrInsertComputed(eventName, makeSet).add({
listener,
internal: options?.internal === INTERNAL_EVT,
once: options?.once === true,
rmAbort,
});Three cases therefore produce an external listener: no options, no internal property, or a value that does not equal the build token. Only the matching token sets the stored internal flag.
Dispatch then uses that flag to create two phases for the same event:
for (const { listener, internal, once } of new Set(eventListeners)) {
if (once) {
this.off(eventName, listener);
}
if (!internal) {
(extListeners ??= []).push(listener);
continue;
}
listener(data);
}
if (extListeners) {
for (const listener of extListeners) {
listener(data);
}
}Internal callbacks run during the first pass; external callbacks are collected and invoked afterward. The source comment states the reason directly: viewer components get time to handle the event and update their state before outside observers run.
This is listener classification and ordering, not two separate buses. External listeners still receive the event. The snapshot created with new Set(eventListeners) also keeps dispatch stable if listeners are added or removed during delivery, and once listeners are removed before invocation.
In the raw-source and GENERIC branches, the EventBus constructor additionally calls Object.seal(this). Its source comment explicitly connects this with preventing third-party method overrides from exposing INTERNAL_EVT. The seal, the build-specific random token, and the strict comparison all support the same internal/external contract.
The Twin File: Keeping Library and Viewer on One Contract
src/shared/internal_evt.js is imported by display-layer code such as src/display/editor/tools.js. Viewer modules and web/event_utils.js import web/internal_evt.js instead. The implementations are identical, and both files carry a comment instructing maintainers to keep them synchronized.
During a build, both sides resolve INTERNAL_EVT through the same build-definition entry, so their strict comparisons use the same UUID. This is why there is no runtime exchange of a secret between the display library and the viewer. The correspondence is established by preprocessing, while the duplicated fallback keeps direct source and testing usage consistent.
The duplication is therefore intentional, but it is not permission for the files to evolve independently. Changing one option shape or fallback without changing the other would break the agreement on which this classification depends.
Summary
The small module supports a precise event-routing mechanism rather than an event system of its own:
- Non-testing builds inject a random UUID through
PDFJSDev.eval; raw source and testing use the fixedinternalEventfallback. INTERNAL_EVTis a comparison token, not an event name, and{ internal: true }cannot substitute for it.internalOptpackages that token in a frozen, reusable options object that composes withsignalandonce.EventBus.onconverts a successful identity check into a stored classification;dispatchinvokes internal listeners before external ones.- The shared and web copies must stay synchronized so display-layer registrations and viewer-side dispatch retain one contract.
The key insight is that build-time injection and event ordering are two halves of the same design. The token establishes which listeners are internal; the dispatcher uses that knowledge to update viewer state before exposing the event to external observers.
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 Deep Dive from Demo 10: CanvasGraphics and operatorList RenderingOctober 27, 2026
- PDF.js Deep Dive from Demo 9: Network Streams and Range RequestsOctober 27, 2026
- PDF.js Deep Dive from Demo 8: PDFWorker.create and the Real/Fake Worker HandshakeOctober 27, 2026
- PDF.js Deep Dive from Demo 7: WorkerTransport InitializationOctober 27, 2026
- PDF.js Deep Dive from Demo 6: MessageHandler Cross-Thread CommunicationOctober 27, 2026
- PDF.js Deep Dive from Demo 5: renderOctober 27, 2026
- PDF.js Deep Dive from Demo 4: getViewportOctober 27, 2026
- PDF.js Deep Dive from Demo 3: getPageOctober 27, 2026
- PDF.js Deep Dive from Demo 2: getDocumentOctober 27, 2026
- PDF.js Source Walkthrough: TextLayerOctober 27, 2026
- PDF.js Source Walkthrough: font_loaderOctober 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_displayOctober 27, 2026
- PDF.js Source Walkthrough: PDFObjectsOctober 27, 2026
- PDF.js Source Walkthrough: Network StreamsOctober 27, 2026
- PDF.js Source Walkthrough: math_clamp.jsOctober 27, 2026
- PDF.js Source Walkthrough: css_utils.jsOctober 27, 2026
- PDF.js Source Walkthrough: scripting_utilsOctober 27, 2026
- PDF.js Source Walkthrough: MurmurHash3_64October 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_utilsOctober 27, 2026
- PDF.js Source Walkthrough: image_utils.jsOctober 27, 2026
- PDF.js Source Walkthrough: BasePDFStreamOctober 27, 2026
- PDF.js Source Walkthrough: Shared UtilitiesOctober 27, 2026
- PDF.js Source Walkthrough: MessageHandlerOctober 27, 2026
- PDF.js Source Walkthrough: apiOctober 27, 2026
- PDF.js Source Walkthrough: fontsOctober 27, 2026
- PDF.js Source Walkthrough: glyphlistOctober 27, 2026
- PDF.js Source Walkthrough: binary_cmapOctober 27, 2026
- PDF.js Source Walkthrough: cmapOctober 27, 2026
- PDF.js Source Walkthrough: encodingsOctober 27, 2026
- PDF.js Source Walkthrough: metricsOctober 27, 2026
- PDF.js Source Walkthrough: standard_fontsOctober 27, 2026
- PDF.js Source Walkthrough: fonts_utilsOctober 27, 2026
- PDF.js Source Walkthrough: default_appearanceOctober 27, 2026
- PDF.js Source Walkthrough: metadata_parserOctober 27, 2026
- PDF.js Source Walkthrough: name_number_treeOctober 27, 2026
- PDF.js Source Walkthrough: struct_treeOctober 27, 2026
- PDF.js Source Walkthrough: catalogOctober 27, 2026
- PDF.js Source Walkthrough: pdf_managerOctober 27, 2026
- 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 from Demo 1: Rendering a PDF with a Custom ViewerOctober 19, 2026
- PDF.js Deep Dive 2: getDocument & getPageOctober 19, 2026
- PDF.js Deep Dive 1: The Architecture of PDF.jsOctober 16, 2026