# How the FileViewer Handles Automatic Refresh for Different File Types in pi-web

> Discover how pi-web's FileViewer uses Server-Sent Events to automatically refresh different file types, ensuring live updates via a bust counter on change events.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: internals
- Published: 2026-08-18

---

**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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/lib/file-types.ts), helper functions classify files by extension:

- `isImagePath()` — routes to **ImageViewer**
- `isAudioPath()` — routes to **AudioViewer**
- `isDocumentPreviewPath()` — 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:

```typescript
// 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:

```typescript
getFileApiUrl(filePath, "read", sourceSessionId, bust ? { v: bust } : undefined)

```

## ImageViewer: Live Image Refresh

The **ImageViewer** creates its SSE connection at lines 76-78:

```typescript
const source = new EventSource(getFileApiUrl(filePath, "watch", sourceSessionId));

```

When a `change` event fires:

1. `syncRequestRef.current` increments to track the request
2. `size` and `naturalSize` state are cleared
3. `bust` counter increments, forcing a new `src` URL 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 `duration` state
- Increments `bust` to 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):

- `bust` invalidates 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:

```typescript
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:

1. **Initialization** — Close any existing `EventSource`, set `watching = false`
2. **Connection** — `new EventSource(getFileApiUrl(filePath, "watch", sourceSessionId))`
3. **Handshake** — On `connected` event, set `watching = true` and perform initial sync
4. **Live updates** — Each `change` event triggers type-appropriate refresh via `bust` invalidation

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`](https://github.com/agegr/pi-web/blob/main/components/FileViewer.tsx) | Central dispatcher with per-type SSE logic |
| [`lib/file-types.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-types.ts) | Extension-based file classification |
| [`lib/file-viewer-state.ts`](https://github.com/agegr/pi-web/blob/main/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`, and `isDocumentPreviewPath` helpers in [`lib/file-types.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-types.ts)
- All viewers create SSE connections to `/api/files/.../watch` using `new EventSource()`
- The **`bust` counter** 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`](https://github.com/agegr/pi-web/blob/main/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.