Skip to content

perf(viewer): isolate PDF subtree, decouple page-nav + jsdom interaction tests#651

Merged
BigSimmo merged 3 commits into
mainfrom
claude/viewer-perf-tests
Jul 14, 2026
Merged

perf(viewer): isolate PDF subtree, decouple page-nav + jsdom interaction tests#651
BigSimmo merged 3 commits into
mainfrom
claude/viewer-perf-tests

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to the document/image viewer review (#619, #643): three render-cost reductions on DocumentViewer plus the jsdom coverage the node/SSR suite can't reach. All changes are frontend/offline — no route, API, CSP, or fetch-contract change.

  • Decouple page navigation from full document reload. The main load effect now distinguishes a full (re)load — new documentId or an explicit retry (previewAttempt) — from navigation on the already-loaded document, via a loadedKeyRef. On navigation it skips the loading reset (no flash), keeps the mounted PDF and current content visible, preserves current content on a transient detail failure, and does not re-issue signed URLs. A full load still resets, re-issues signed URLs, and surfaces errors exactly as before.
  • Narrow the body-wide MutationObserver. #main-content (the app-shell scroll container) is resolved synchronously; the observer only watches the DOM until it appears, then disconnects — instead of reacting to every app-wide mutation for the viewer's lifetime.
  • Isolate the heaviest subtree. PdfCanvasViewer (holds the pdf.js document + canvas raster), NativePdfEmbed, and NonPdfSourcePreview are wrapped in memo, and the two PDF callbacks (onUrlExpired/onLoadSuccess) are stabilised with useCallback. Unrelated parent re-renders — source-search keystrokes, composer focus, connectivity changes — no longer re-render or re-rasterise the viewer.
  • Tests. New jsdom specs for SignedImage (fetch → failed → retry → recover, plus click-to-open lightbox that closes on Escape) and useViewerGestures (Ctrl/⌘-wheel gating, lightbox no-modifier mode, wheelZoom off; pointer pan frame-deltas, pinch distance-ratio zoom, touchPan opt-out).

Verification

  • npm run lint (repo, --max-warnings 0) — clean
  • npm run typecheck (tsc --noEmit) — clean
  • npx vitest run — 2295 passed / 3 skipped (incl. the 2 new dom specs, 8 tests)
  • npx prettier --check on all touched files — clean
  • npm run verify:pr-local — not run this session (heavier gate); the constituent lint/typecheck/format/vitest gates above all pass
  • npm run verify:ui — not run; changes are render-cost/isolation only (memoisation + effect gating), no visual/DOM output change. The viewer's visual behaviour was driven in real Chromium during the parent PRs (Optimise and extend the document & image viewers #619/Signed-URL-only refresh for expired PDFs (shared fetch, abortable) #643).

No retrieval/ranking/selection/chunking/citation or answer-contract behaviour changed, so eval:* gates are not applicable.

Clinical Governance Preflight

The page-nav decouple touches the document-detail load + signed-URL re-issue pipeline (document access), so completed conservatively:

  • Source-backed claims still require linked source verification before clinical use — unchanged; no answer/source-governance logic touched
  • No patient-identifiable document workflow was introduced or expanded
  • Supabase target remains Clinical KB Database (sjrfecxgysukkwxsowpy) — no env/config change
  • Service-role keys and private document access remain server-only — signed-URL fetch/epoch/abort logic is preserved; only when it runs was gated (full reload vs navigation)
  • Demo/synthetic content remains clearly separated from real clinical sources — unchanged
  • Source metadata, review status, and outdated/unknown-source behavior remain conservative — unchanged
  • Deployment classification/TGA SaMD impact — no clinical decision-support behaviour changed (viewer render-cost only)

Notes

  • Second commit is tests-only; first is the perf/render-isolation change — reviewable independently.
  • InlineImagePreview (inside NonPdfSourcePreview) stays keyed by signedUrl, so a freshly issued URL still remounts with a clean failure state under the new memo boundary.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DqMTk5fDNgi5GwEK5peRmj


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved document viewer loading to distinguish full reloads from in-page navigation, preserving the current view during transient failures.
    • Enhanced recovery for expired PDF preview/download links and more reliable signed-image URL retries.
    • Improved document viewer layout synchronization with the main content area.
  • Performance
    • Reduced unnecessary re-rendering of PDF and non-PDF preview components.
  • Tests
    • Added/expanded DOM coverage for signed image retry + lightbox, viewer gesture handling (wheel/pointer/touch), and viewer navigation load-state logic.

claude added 2 commits July 14, 2026 10:04
Three render-cost reductions on the document viewer, all frontend/offline:

- Page/chunk navigation no longer triggers a full document reload. The main
  load effect now distinguishes a full (re)load — new documentId or explicit
  retry — from navigation on the already-loaded document via a loadedKeyRef.
  Navigation skips the loading reset, keeps the mounted PDF/content on screen
  (no flash), preserves current content on a transient detail failure, and
  does not re-issue signed URLs.

- The app-shell scroll container (#main-content) is resolved synchronously and
  the MutationObserver only watches until it appears, then disconnects — rather
  than reacting to every app-wide DOM mutation for the viewer's lifetime.

- The heaviest subtree (PdfCanvasViewer, holding the pdf.js document and the
  canvas raster) plus NativePdfEmbed and NonPdfSourcePreview are wrapped in
  memo, and the two PDF callbacks (onUrlExpired/onLoadSuccess) are stabilised
  with useCallback. Unrelated parent re-renders — source-search keystrokes,
  composer focus, connectivity changes — no longer re-render or re-rasterise
  the viewer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqMTk5fDNgi5GwEK5peRmj
…r jsdom

Add jsdom coverage for the effect-driven viewer paths the node/SSR suite can't
reach:

- signed-image.dom.test.tsx: drives the SignedImage fetch → failed → retry →
  recover flow (jsdom omits IntersectionObserver, so the image loads
  immediately) and the click-to-open lightbox that closes on Escape. The
  useAuthSession mock returns stable references so the fetch effect isn't
  re-run on every render.

- use-viewer-gestures.dom.test.tsx: asserts the native non-passive wheel branch
  (Ctrl/⌘ gating, lightbox no-modifier mode, wheelZoom off) and the pointer
  pan/pinch maths (frame-delta panning, distance-ratio zoom, touchPan opt-out).

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

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4d63849e-e524-401c-aca7-e76ac75641fa

📥 Commits

Reviewing files that changed from the base of the PR and between fbf3f18 and e82a811.

📒 Files selected for processing (3)
  • src/components/DocumentViewer.tsx
  • src/lib/document-viewer-navigation.ts
  • tests/document-viewer-navigation-source.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/DocumentViewer.tsx

📝 Walkthrough

Walkthrough

DocumentViewer now separates full reloads from navigation, preserves state and signed URLs during navigation, and observes DOM mutations for scroll-container setup. Viewer components are memoized, while signed-image and gesture interactions gain jsdom coverage.

Changes

Document viewer behavior

Layer / File(s) Summary
Load identity and document recovery
src/lib/document-viewer-navigation.ts, src/components/DocumentViewer.tsx, tests/document-viewer-navigation-source.test.ts
Load keys distinguish full reloads from navigation, control signed-URL refreshes and failure handling, and support stable PDF expiry callbacks; helper behavior is tested.
Scroll container synchronization
src/components/DocumentViewer.tsx
A MutationObserver waits for #main-content, assigns the scroll container, and disconnects after discovery.

Memoized viewer components

Layer / File(s) Summary
Memoize viewer exports
src/components/document-viewer/non-pdf-source-preview.tsx, src/components/document-viewer/pdf-canvas-viewer.tsx
NonPdfSourcePreview, PdfCanvasViewer, and NativePdfEmbed are wrapped with memo without changing rendering logic.

DOM interaction tests

Layer / File(s) Summary
DOM interaction coverage
tests/signed-image.dom.test.tsx, tests/use-viewer-gestures.dom.test.tsx
Tests cover signed-image retry and lightbox behavior, wheel zoom modifiers, disabled zoom, mouse panning, touch pinch zoom, pointer release, and disabled touch panning.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • BigSimmo/Database#643: Both changes modify DocumentViewer signed-URL loading and expiry recovery behavior.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main changes: viewer performance isolation, page navigation decoupling, and added jsdom interaction tests.
Description check ✅ Passed The description follows the template with Summary, Verification, Clinical Governance Preflight, and Notes, and the sections are substantially filled out.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/viewer-perf-tests

Comment @coderabbitai help to get the list of available commands.

@supabase

supabase Bot commented Jul 14, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project sjrfecxgysukkwxsowpy because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@BigSimmo
BigSimmo marked this pull request as ready for review July 14, 2026 10:27
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/DocumentViewer.tsx (1)

1901-1970: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

No targeted test for the new full-reload vs navigation branching.

This effect's behavior change (isFullReload/loadedKeyRef gating resets, signed-URL refetch, and error propagation) is exactly the kind of logic a regression like the one flagged above would slip through untested — the PR's new tests cover SignedImage and useViewerGestures only, not this branching. As per coding guidelines, "When behavior changes, add or update the smallest relevant targeted test and run the narrowest relevant validation before broader suites."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/DocumentViewer.tsx` around lines 1901 - 1970, Add a focused
test for the DocumentViewer loading effect covering both full reload and
navigation behavior: verify full reload failures clear viewer content and set
the appropriate error, while transient navigation detail failures preserve
existing content; also cover signed-URL application and stale/aborted request
handling as relevant. Use the existing DocumentViewer test setup and run the
narrowest targeted test command.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/DocumentViewer.tsx`:
- Around line 1849-1850: Update the settle-handler around the detail-load result
and loadedKeyRef.current so the key is stamped only when the detail load
succeeds, specifically when detailResult.status indicates success. Keep failed
detail loads and signed-URL failures treated as full reloads so subsequent
navigation re-fetches URLs and updates viewer errors.

---

Nitpick comments:
In `@src/components/DocumentViewer.tsx`:
- Around line 1901-1970: Add a focused test for the DocumentViewer loading
effect covering both full reload and navigation behavior: verify full reload
failures clear viewer content and set the appropriate error, while transient
navigation detail failures preserve existing content; also cover signed-URL
application and stale/aborted request handling as relevant. Use the existing
DocumentViewer test setup and run the narrowest targeted test command.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 379b9f9e-2c44-40c2-9d8e-82cc2581fabf

📥 Commits

Reviewing files that changed from the base of the PR and between 570e6ba and fbf3f18.

📒 Files selected for processing (5)
  • src/components/DocumentViewer.tsx
  • src/components/document-viewer/non-pdf-source-preview.tsx
  • src/components/document-viewer/pdf-canvas-viewer.tsx
  • tests/signed-image.dom.test.tsx
  • tests/use-viewer-gestures.dom.test.tsx

Comment thread src/components/DocumentViewer.tsx Outdated
A failed full load stamped loadedKeyRef unconditionally, so the next page/chunk
navigation on the same document computed isFullReload=false and wrongly skipped
the signed-URL re-fetch and the viewer-error refresh — a document that never
loaded could never recover except via an explicit "Retry preview". (fetchSignedUrlPair
uses allSettled and never rejects, so this path is reached, not caught.)

Advance the loaded key only when the detail request actually succeeds; a failed
load keeps the previous key so subsequent navigation is still treated as a full
reload and retries the recovery work.

Extract the reload-key decision into pure documentLoadKey / isFullDocumentReload
/ nextLoadedDocumentKey helpers in document-viewer-navigation.ts and cover the
full-reload-vs-navigation gating (incl. the failed-load regression guard) in the
node suite — the branching was previously untested.

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

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Autofix skipped. No unresolved CodeRabbit review comments with fix instructions found.

@BigSimmo
BigSimmo merged commit f0eec3e into main Jul 14, 2026
24 checks passed
@BigSimmo
BigSimmo deleted the claude/viewer-perf-tests branch July 14, 2026 11:15
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