Skip to content

UN-4502 [DEV] Add the v1 PDF loader, document hook and viewer states - #2404

Open
muhammad-ali-e wants to merge 7 commits into
UN-4299-design-tokens-themesfrom
UN-4502-v1-pdf-loader
Open

muhammad-ali-e wants to merge 7 commits into
UN-4299-design-tokens-themesfrom
UN-4502-v1-pdf-loader

Conversation

@muhammad-ali-e

@muhammad-ali-e muhammad-ali-e commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

What

  • src/v1/lib/pdf/pdfjs.ts and types.ts: the only place v1 imports pdf.js.
  • src/v1/hooks/usePdfDocument.ts: opens a PDF and tracks it through each state.
  • src/v1/components/patterns/document-viewer/ViewerStatus.tsx: renders every state except ready. The page list renders ready (UN-4503).

First of the P1-T21 DocumentViewer sub-tasks (UN-4390).

Why

Six v1 page tasks need one document viewer. This PR is its foundation: one way to load a PDF, with the security defaults set once.

How

  • Uses the pdfjs-dist@3.11.174 already in package.json, as Jaseem asked: one pdf.js for the whole frontend.
    • It imports the same default build and worker that v0 ships, so the app has one pdf.js engine and one worker.
    • The worker is bundled with ?url, never fetched from a CDN.
    • Both load on first use (dynamic import), so pdf.js stays out of any bundle that never opens a PDF.
  • getDocument always gets isEvalSupported: false (CVE-2024-4367; 3.x has no other fix).
  • Sources are {kind: "url", url, withCredentials?, httpHeaders?} or {kind: "data", data}. In-memory bytes are copied first, because pdf.js transfers its buffer to the worker and would empty the caller's copy before a Retry.
  • Page sizes are read in parallel, at most 16 at a time. The spike measured about 1 s for 200 pages when they were read one after another.
  • usePdfDocument(source) returns {state, retry, submitPassword}. state is one of: empty, loading, password (with an incorrect flag), unsupported, failed (with its message) or ready (document and pageSizes).
    • It reopens only when the URL, the request options or the byte buffer change, so callers can pass a fresh object on every render.
    • The previous loading task is destroyed when the source changes and on unmount, and its late result is ignored.
  • Load errors are classified by pdf.js exception name:
    • PasswordException code 1 = password needed, code 2 = wrong password
    • InvalidPDFException = unsupported
    • anything else = failed
  • The status messages use ui: tokens only. There's no core Button yet, so these are plain elements with the focus ring. They'll swap to the core component once it exists.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No. Everything is new code under src/v1/, and nothing in v0 or the v1 entry imports it yet. There are no package or config changes, and v0's pdf.js setup is untouched. v1 shares v0's pdf.js module and sets the same GlobalWorkerOptions.workerSrc (the bundled pdf.worker.min.js URL), so loading either one first leaves the other working.

Database Migrations

  • None

Env Config

  • None

Relevant Docs

  • Plan and folder mapping: P1-T21 implementation plan (UN-4390). Spike results are on UN-4501.

Related Issues or PRs

Dependencies Versions

  • None. It uses the existing pdfjs-dist@3.11.174.

Notes on Testing

  • bun run typecheck:v1, bun run lint:v1 and bun run test:v1 pass: 65 unit tests and 56 boundary fixtures. biome ci src/ reports no errors.
  • The new tests mock pdf.js:
    • eval is always off
    • bytes are copied
    • the source key is stable
    • page sizes are read in parallel and returned in order
    • error classification
    • destroy runs once, and a load destroyed before pdf.js arrives never starts
    • every hook state transition: Retry, password missing then wrong then right, source change, unmount
    • the status component's states and accessibility
  • Real rendering against a 200-page fixture was checked in the UN-4501 spike on this same pdf.js version. Browser tests come in UN-4507.

Screenshots

  • None. There's no page to mount on yet; the gallery comes in UN-4507.

Checklist

I have read and understood the Contribution Guidelines.


Generated by Claude Code

…states

- lib/pdf/pdfjs.ts is the only v1 import of pdf.js: the existing
  pdfjs-dist 3.11.174 legacy build, its worker bundled with ?url, both
  loaded on first use. getDocument always gets isEvalSupported: false
  (CVE-2024-4367), and in-memory bytes are copied before the worker
  transfer empties them.
- Page sizes are read in parallel, bounded at 16 in flight.
- hooks/usePdfDocument tracks empty, loading, password (missing or
  wrong), unsupported, failed (with Retry) and ready. It reopens only
  when the source's URL, request options or bytes change, and destroys
  the previous load on change and on unmount.
- components/patterns/document-viewer/ViewerStatus renders every state
  except ready.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm

Copy link
Copy Markdown
Contributor Author

pre-commit.ci - pr is red because of the base branch, not this PR.

The failing hook is check-shebang-scripts-are-executable, on frontend/scripts/check-v1-styles.mjs. That file has a #! line but no executable bit. It comes from the tokens PR #2399 (UN-4299-design-tokens-themes); this PR doesn't touch it. The hook doesn't fail on #2399 itself only because pre-commit.ci doesn't run on that PR. Running pre-commit run --from-ref $(git merge-base origin/main HEAD) locally reproduces it, and this PR's own files pass every hook.

There's no fix to port yet. The fix is a one-line change on #2399: git add --chmod=+x frontend/scripts/check-v1-styles.mjs (or drop the shebang). Once #2399 has it, this check should go green here without any change to this PR.


Generated by Claude Code

pdf.js puts the full URL in MissingPDFException and
UnexpectedResponseException messages, so a signed URL's token reached the
screen. Build those messages from the status instead. Also cover the
reset that lets Retry load pdf.js again after a failed chunk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
@muhammad-ali-e
muhammad-ali-e marked this pull request as ready for review November 2, 2026 06:59
@greptile-apps

greptile-apps Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

via Greptile

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The reviewed changes appear safe to merge.

Summary

Adds the v1 PDF loader, document hook, and viewer status screens.

  • The PDF loader opens sources and returns ordered page sizes.
  • The PDF hook tracks each load and lets callers retry or answer password prompts.
  • The viewer shows a clear message for each state before a PDF is ready.

Reviews (4) · Last reviewed commit: "UN-4502 Load the same pdf.js build and w..." · Reviewed by Greptile

Submitting swaps the prompt for the loading state, so the field lost focus
and a keyboard user had to find it again. Raised by Greptile on #2404.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
greptile-apps[bot]
greptile-apps Bot previously approved these changes Nov 2, 2026

@praveen-formido praveen-formido left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed as the base of the stack (lens: v1 styles and tokens, plus the loader logic). On the stack tip (68af05717) typecheck:v1, lint:v1 and the 173 v1 tests pass, and the current check-v1-styles.mjs from #2399 reports clean, so the --*-only style objects are fine throughout.

Two things I would fix before this goes in: the password/retry path re-downloads the document (comment on usePdfDocument.ts), and the legacy pdf.js build puts a second engine and worker in the bundle (comment on pdfjs.ts). The rest are smaller.

Stack note: these five fork from #2399 at 8028a7320, before its last commits. Once #2399 merges they retarget to main and need a rebase onto it (for example git rebase --onto main 8028a7320 from the bottom of the stack), otherwise the diffs keep showing the #2399 commits.

Comment thread frontend/src/v1/hooks/usePdfDocument.ts Resolved
Comment thread frontend/src/v1/hooks/usePdfDocument.ts Resolved
Comment thread frontend/src/v1/lib/pdf/pdfjs.ts Resolved
Comment thread frontend/src/v1/lib/pdf/pdfjs.ts Resolved
Comment thread frontend/src/v1/lib/pdf/pdfjs.ts Resolved
Comment thread frontend/src/v1/lib/pdf/pdfjs.ts Resolved
Comment thread frontend/src/v1/hooks/usePdfDocument.ts Resolved
…iling the open

From review on #2404:
- A password goes to pdf.js's onPassword callback on the live loading task,
  so a password (right or wrong) no longer downloads the file again or starts
  a new worker, and Retry carries no stale password.
- A page whose size cannot be read borrows a neighbour's size and is logged;
  only a document with no readable page fails to open.
- An unknown pdf.js error shows a plain message and is logged, instead of
  showing pdf.js's own wording.
- destroy() handles the rejection pdf.js gives when the worker is gone.
- The hook's request type is OpenRequest, not a shadow of fetch's Request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
@greptile-apps
greptile-apps Bot dismissed their stale review November 3, 2026 09:53

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Nov 3, 2026
…uild

The legacy build put a second pdf.js engine and worker next to v0's. v1 now
imports pdfjs-dist and its default worker, the copy v0 already ships.

Also hands the rejected destroy promise in the loader test a handler up front,
so Vitest no longer reports it as an unhandled rejection while the test waits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
@greptile-apps
greptile-apps Bot dismissed their stale review November 3, 2026 11:20

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@sonarqubecloud

sonarqubecloud Bot commented Nov 3, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants