How the FileViewer Handles Automatic Refresh for Different File Types in pi-web
The FileViewer component in pi-web implements automatic refresh through Server-Sent Events (SSE), where each specialized viewer maintains a live connection to /api/files/.../watch and invalidates cached content via a bust counter when change events occur.
The FileViewer component in agegr/pi-web provides a unified live-refresh system across image, audio, document, and text file types. This article explains how automatic refresh for different file types is implemented, based on the source code in components/FileViewer.tsx and supporting files.
File Type Detection and Viewer Selection
Before handling refresh logic, the component determines which viewer to render. In lib/file-types.ts, helper functions classify files by extension:
isImagePath()— routes to ImageViewerisAudioPath()— routes to AudioViewerisDocumentPreviewPath()— routes to DocumentViewer- Default fallback — TextFileViewer
Each viewer implements the same SSE-based refresh pattern but adapts it to its specific content type.
Core Refresh Mechanism: The bust Counter
All viewers rely on a shared state variable called bust (a simple integer counter). When the server notifies the client of file changes, setBust(b => b + 1) triggers, forcing React to generate new URLs and re-fetch content.
This pattern appears consistently across viewers:
// From ImageViewer section (lines 84-92)
setBust(b => b + 1); // Forces new <img src> with cache-busting query param
The bust value is appended as a v query parameter:
getFileApiUrl(filePath, "read", sourceSessionId, bust ? { v: bust } : undefined)
ImageViewer: Live Image Refresh
The ImageViewer creates its SSE connection at lines 76-78:
const source = new EventSource(getFileApiUrl(filePath, "watch", sourceSessionId));
When a change event fires:
syncRequestRef.currentincrements to track the requestsizeandnaturalSizestate are clearedbustcounter increments, forcing a newsrcURL for the<img>element
The toolbar displays a live-sync indicator (green when watching is true, red otherwise) at lines 31-45.
AudioViewer: Duration and Source Reload
The AudioViewer follows an identical SSE setup at lines 48-50. Its change handler (lines 55-63) performs three actions:
- Clears cached
durationstate - Increments
bustto reload<audio src> - Updates file size metadata
This ensures the audio element fetches fresh content without manual page reload.
DocumentViewer: PDF and Office Document Preview
For PDF, DOCX, and other documents, the viewer at lines 90-92 establishes the SSE connection. On change (lines 98-106):
bustinvalidates the preview cache- Size metadata refreshes
- The
<iframe>or embedded viewer re-loads with the updated document
TextFileViewer: Content and Diff Synchronization
The TextFileViewer has the most complex refresh logic. Its SSE connection is established in a useEffect hook watching filePath (lines 115-122).
On change, it executes two parallel fetches:
fetchContent(); // Reload source text
fetchGitDiff(); // Update diff view
The bust counter ensures raw file fetches bypass browser cache via the v query parameter (lines 130-132).
Server-Sent Events Lifecycle
All viewers follow this standardized SSE workflow:
- Initialization — Close any existing
EventSource, setwatching = false - Connection —
new EventSource(getFileApiUrl(filePath, "watch", sourceSessionId)) - Handshake — On
connectedevent, setwatching = trueand perform initial sync - Live updates — Each
changeevent triggers type-appropriate refresh viabustinvalidation
The watch endpoint implementation resides in app/api/files/[...path]/route.ts, emitting SSE events for connected and change states.
Supporting Infrastructure
| File | Purpose |
|---|---|
components/FileViewer.tsx |
Central dispatcher with per-type SSE logic |
lib/file-types.ts |
Extension-based file classification |
lib/file-viewer-state.ts |
Persisted viewer preferences (display mode, scroll position, wrap lines) |
app/api/files/[...path]/route.ts |
SSE watch endpoint implementation |
Summary
- FileViewer selects specialized viewers via
isImagePath,isAudioPath, andisDocumentPreviewPathhelpers inlib/file-types.ts - All viewers create SSE connections to
/api/files/.../watchusingnew EventSource() - The
bustcounter provides uniform cache invalidation across image, audio, document, and text content - TextFileViewer uniquely refreshes both content and Git diffs on change events
- The live-sync indicator visualizes connection state in each viewer's toolbar
Frequently Asked Questions
How does FileViewer know when a file has changed on disk?
The FileViewer does not poll. Instead, each viewer opens a persistent Server-Sent Events connection to the backend watch endpoint (/api/files/.../watch). When the filesystem detects a modification, the server pushes a change event through the SSE stream, triggering client-side refresh.
Why does the ImageViewer use a bust counter instead of directly modifying the <img src?
Incrementing a counter (setBust(b => b + 1)) and embedding it as a query parameter (?v=3) forces the browser to treat each version as a unique URL. This reliably bypasses image caching without complex state management or DOM manipulation.
What file types support live refresh in pi-web?
According to lib/file-types.ts, live refresh covers images (via isImagePath), audio (via isAudioPath), documents including PDF and DOCX (via isDocumentPreviewPath), and all other files through the fallback TextFileViewer.
Does the TextFileViewer refresh differently than other viewers?
Yes. While ImageViewer, AudioViewer, and DocumentViewer primarily invalidate cached previews, TextFileViewer explicitly calls fetchContent() and fetchGitDiff() on change events. This dual fetch ensures both raw file content and version control diffs remain synchronized.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →