Repository navigation
Description
Buyer-visible outcome
Prevent an older PDF.js render from overwriting or interleaving with a newer viewer refresh/navigation. A buyer must never see stale document pixels or a stale Ready. state after a newer request has started, failed, changed document identity, or been cancelled.
Current implementation status — 2026-08-10
Protected main remains 83ec6f7fe2b04bdcd28bf98ec350e41e55730a18 and does not yet include this lifecycle repair.
PR #323 is the current bounded first slice at exact head 79689f928d8552c65dbdecf85ed45e2e9a6f1109. It is Ready/mergeable with exact-head CI 31374483914, Security Scan 31374483892, SAST Semgrep 31374483824, and fuzz 31374483908 successful; an exact-head independent review has been requested. Until it integrates, it is ACTIVE_PR evidence only.
#323 closes the critical stale DOM/state publication part of this issue: renderPdfInline(path, abortSignal) now checks cancellation around PDF.js loading/page/render boundaries and returns before appending stale canvas/metadata; its caller also stops later artifact-link/Ready publication after supersession. The executable Node regression holds an old page.render().promise, aborts it, completes it late, and proves no stale publication.
Do not close this issue when #323 merges. The original contract also requires active cancellation/destruction of superseded PDF.js work where supported, signed-token-mode lifecycle parity, multi-generation/error-state cases, and broader newest-generation ownership evidence. The current #323 implementation discards late results safely but does not yet invoke PDF.js RenderTask.cancel() or explicitly destroy an in-flight PDFDocumentLoadingTask on supersession. Keep those as successor acceptance after the bounded stale-publication slice is protected.
Protected-main problem evidence
src/main/resources/static/assets/viewer/viewer.js on protected main uses an AbortController around status/bootstrap fetch calls, but PDF.js loading/rendering has no operation-generation authority. A later Refresh can therefore supersede network work without controlling the older render continuation.
This path is separate from issue #317's demo-auth removal: #317 owns production identity/bootstrap semantics, while this issue owns asynchronous rendering and UI state authority.
Required lifecycle contract
Introduce one viewer-operation generation/cancellation boundary that covers network, PDF loading, page rendering, DOM publication, and terminal UI state.
At minimum:
- Every refresh/init render obtains an immutable operation generation or equivalent cancellation token.
- Starting a newer operation invalidates the older generation before new work begins.
renderPdfInlinereceives the operation authority and checks it before every externally visible DOM/state publication.- Where PDF.js exposes supported cancellation/destruction for the loading/render task, cancel the old task as part of supersession; cancellation failure must not authorize stale DOM publication.
- An older operation may not append canvas, metadata, artifact links, error state,
Ready., retry-button state, oraria-busy=falseafter supersession. - A superseded operation exits silently rather than presenting a network/rendering error caused solely by cancellation.
- Exactly one current operation owns terminal viewer status at a time.
- Signed-token mode and authenticated bootstrap mode use the same generation-safe publication invariant.
Do not solve the race by globally serializing the UI until all old rendering finishes; the user must be able to refresh/retry promptly while stale work is safely discarded.
Test-first acceptance
Required cases across the current and successor slices:
- first status/bootstrap reaches PDF rendering; user starts Refresh before the first
page.render().promiseresolves; second operation completes first; resolving the first render afterward does not alter the second operation's DOM/status; - old render rejection after supersession does not replace a newer successful state with an error;
- old success after a newer failure does not replace the newer failure with
Ready.; - rapid three-generation retry sequence publishes only the newest canvas/link/metadata;
aria-busy, retry disabled/text state, live region/status, and focus remain owned by the newest generation;- signed
artifactTokenmode obeys the same stale-publication rule; - cancellation/destruction is invoked when the supported PDF.js seam is available, while stale-publication rejection remains the fail-safe authority;
- no unhandled promise rejection during supersession;
- new production JS behavior has exact owned statement/branch coverage under the repository's applicable JavaScript test gate and beginner-readable comments/docs for the lifecycle boundary.
Security / privacy
Stale rendering is also an isolation problem: after future production workspace/auth work from #317, a late render from a superseded tenant/session must never publish pixels or metadata into the current viewer state. The generation authority must remain local and opaque; do not encode tenant IDs, tokens, document bytes, or raw authorization state into logs or public DOM attributes merely to identify a render.
Dependency / writer boundary
- Do not edit
demo.jsordom-utils.jswhile fix(accessibility): make asynchronous viewer actions nested-safe #264 owns those paths. - Do not edit fix(security): harden audit pseudonymization and refresh Netty evidence #270-owned Java/controller/build paths merely to install this fix.
- Do not churn fix(viewer): suppress stale PDF render publication #323 after it is Ready/reviewing just to add the remaining resource-cancellation work; integrate or deliberately supersede that bounded slice first, then implement the next path-safe successor from the protected baseline.
- If issue [Product Gap] Remove buyer-demo shell and hard-coded demo authority from production UI #317 later changes
viewer.js, reconcile both concerns deliberately: identity/bootstrap determines what operation is authorized; this issue determines which generation may publish viewer state.
Documentation / release
PR #305 remains the canonical cross-cutting documentation writer. Reconcile test strategy/operability/traceability after the implementation reaches a reviewed integration line. Release acceptance must include deterministic stale-generation and cancellation/resource-cleanup regressions rather than relying on timing luck.
Activity
chore: abort superseded work