Attach (recommended) or Link to PDF file
Not needed, no document is involved.
Web browser and its version
Not browser-specific; reproduced with pdfjs-dist's web/pdf_viewer.mjs in Node.js 24 with happy-dom, same in Chromium 153 with the viewer components.
Operating system and its version
macOS 26.5
PDF.js version
6.4.299
Is the bug present in the latest PDF.js version?
Yes
Is a browser extension
No
Steps to reproduce the problem
Using the viewer components (pdf_viewer.mjs), create a PDFScriptingManager without externalServices (the documented way, it then creates the GenericScripting itself), tear it down and dispatch the sandbox event on window:
import { EventBus, PDFScriptingManager } from 'pdfjs-dist/web/pdf_viewer.mjs'
async function openAndClose(name) {
const eventBus = new EventBus()
eventBus.on('updatefromsandbox', () => console.log(`${name}: eventBus still receives updatefromsandbox`))
const manager = new PDFScriptingManager({ eventBus })
await manager.setDocument(null) // like a viewer that is torn down
}
await openAndClose('viewer 1')
await openAndClose('viewer 2')
await openAndClose('viewer 3')
window.dispatchEvent(new CustomEvent('updatefromsandbox', { detail: {} }))
What is the expected behavior?
Nothing is logged: a scripting manager that was torn down (or garbage collected) no longer listens on window, and nothing keeps its EventBus alive.
What went wrong?
All three managers log:
viewer 1: eventBus still receives updatefromsandbox
viewer 2: eventBus still receives updatefromsandbox
viewer 3: eventBus still receives updatefromsandbox
PDFScriptingManagerComponents adds the listener in its constructor when no externalServices are given:
if (!options.externalServices) {
window.addEventListener("updatefromsandbox", event => {
options.eventBus.dispatch("updatefromsandbox", { source: window, detail: event.detail });
});
}
It is never removed: #destroyScripting aborts the manager's own #eventAC, but this listener is registered outside of it, and there is no destroy() that could remove it either. In a single-page app that opens documents one after another (new PDFViewer + PDFScriptingManager + EventBus per document), every closed document stays reachable through window → listener → eventBus → its listeners (viewer, link service, …). It also forwards sandbox events of the current document to all previous event buses.
A fix could register the listener with the manager's abort signal (or in setDocument instead of the constructor, removed in #destroyScripting). The workaround for embedders is to pass their own externalServices.createScripting, which skips the listener.
Attach (recommended) or Link to PDF file
Not needed, no document is involved.
Web browser and its version
Not browser-specific; reproduced with
pdfjs-dist'sweb/pdf_viewer.mjsin Node.js 24 with happy-dom, same in Chromium 153 with the viewer components.Operating system and its version
macOS 26.5
PDF.js version
6.4.299
Is the bug present in the latest PDF.js version?
Yes
Is a browser extension
No
Steps to reproduce the problem
Using the viewer components (
pdf_viewer.mjs), create aPDFScriptingManagerwithoutexternalServices(the documented way, it then creates theGenericScriptingitself), tear it down and dispatch the sandbox event onwindow:What is the expected behavior?
Nothing is logged: a scripting manager that was torn down (or garbage collected) no longer listens on
window, and nothing keeps itsEventBusalive.What went wrong?
All three managers log:
PDFScriptingManagerComponentsadds the listener in its constructor when noexternalServicesare given:It is never removed:
#destroyScriptingaborts the manager's own#eventAC, but this listener is registered outside of it, and there is nodestroy()that could remove it either. In a single-page app that opens documents one after another (newPDFViewer+PDFScriptingManager+EventBusper document), every closed document stays reachable throughwindow→ listener →eventBus→ its listeners (viewer, link service, …). It also forwards sandbox events of the current document to all previous event buses.A fix could register the listener with the manager's abort signal (or in
setDocumentinstead of the constructor, removed in#destroyScripting). The workaround for embedders is to pass their ownexternalServices.createScripting, which skips the listener.