You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
|**Enter on folder**|`FilePane.handleNavigate` → `loadDirectory`|`DualPaneExplorer.applyPathChange` pushes history AFTER `listing-complete`| History push on success only |
85
+
|**Back/forward**|`DualPaneExplorer.handleNavigationAction` → `setPanePath` → FilePane `$effect` → `loadDirectory`|`updatePaneAfterHistoryNavigation` moves history BEFORE load | Optimistic — if path is gone, error handler resolves upward |
86
+
|**Volume switch**|`VolumeBreadcrumb.onVolumeChange` → `FilePane.loadDirectory` + `DualPaneExplorer.handleVolumeChange`| Pushed immediately in `handleVolumeChange`| Optimistic — `determineNavigationPath` may correct to a better path in background |
87
+
88
+
**Old listing cleanup** (in `FilePane.loadDirectory`, every navigation):
89
+
90
+
1.`++loadGeneration` — invalidates all in-flight events
91
+
2.`cancelListing(oldId)` → sets `AtomicBool` in Rust → background task stops within ~100ms
92
+
3.`listDirectoryEnd(oldId)` → stops file watcher, removes from `LISTING_CACHE`
93
+
4. Unlisten all 6 event listeners
94
+
5. Generate new `listingId` (frontend `crypto.randomUUID()`), subscribe new listeners, call `listDirectoryStart`
95
+
96
+
**Volume switch specifics**: `handleVolumeChange` saves the old volume's `lastUsedPath` immediately (no debounce),
97
+
then runs `determineNavigationPath` in background (each check has 500ms timeout). A `volumeChangeGeneration` counter
98
+
guards against stale corrections if the user switches again.
99
+
100
+
### Listing lifecycle
101
+
102
+
```
103
+
Frontend (FilePane) Rust backend (streaming.rs)
104
+
| |
105
+
|-- loadDirectory(path) |
106
+
| listingId = randomUUID() |
107
+
| listDirectoryStart(...) ------------>| spawn tokio task → spawn_blocking
108
+
|<-- { listingId, status: Loading } |
109
+
| |-- emit listing-opening
110
+
| |-- volume.list_directory_with_progress()
111
+
|<-- listing-progress (every 200ms) -----| (on separate OS thread, polls cancel every 100ms)
|`loadGeneration`|`FilePane` (per-instance) |`loadDirectory()`, `adoptListing()`| All 6 listing event handlers; post-`listDirectoryStart` staleness check | Stale listing events from previous navigation applied to current |
134
+
|`listingId` match |`FilePane` (per-instance) | Set to new UUID each `loadDirectory()`| Every event handler (belt-and-suspenders with `loadGeneration`) | Wrong listing's events applied to a different navigation |
135
+
|`volumeChangeGeneration`|`DualPaneExplorer` (singleton) |`handleVolumeChange()`|`determineNavigationPath` callback | Stale path corrections applied after user switched volumes again |
136
+
|`cacheGeneration`|`FilePane` → prop to `FullList`/`BriefList`|`refreshView()`, `adoptListing()`, `directory-diff` handler | Virtual scroll `$effect` via `shouldResetCache()`| Stale frontend scroll cache after sort/filter/watcher changes |
137
+
|`AtomicBool` (Rust) |`StreamingListingState` per listing |`cancel_listing()` IPC | Per-entry during read, every 100ms poll, at cache-insert (under write lock) | Cancelled listing inserting into cache or continuing I/O |
138
+
139
+
### Cancellation patterns
140
+
141
+
Three architectural patterns used consistently:
142
+
143
+
1.**`AtomicBool` flag (Rust)**: Every long-running backend task (listing, copy/move/delete, scan, search, indexing,
144
+
AI download) uses an `AtomicBool` checked at iteration boundaries. Stored in global `HashMap` keyed by operation ID.
145
+
2.**Generation counter (TypeScript)**: Incremented on each new request. Stale responses silently discarded.
146
+
Lightweight, no backend coordination needed. Used for `loadGeneration` and `currentFetchId` (viewer).
|**Mid-stream I/O error**| Rust error through channel → `listing-error`| Spinner → auto-navigates to parent | Same as "path deleted" | No partial cache (listing is atomic — all or nothing) |
204
+
|**Volume unmounted**|`volume-unmounted` Tauri event (dedicated handler) | Pane switches to home directory | Hard switch to root volume + `~`| Full pane state overwrite + persist |
205
+
|**MTP disconnect**|`mtp-device-removed` event | Falls back to default volume |`handleMtpFatalError` → root volume + `~`| Same as volume unmount |
206
+
207
+
**Per-entry permission errors** (single unreadable file in a readable dir) don't fail the listing — they appear as
208
+
zero-permission entries with fallback metadata.
209
+
76
210
### Persistence
77
211
78
212
-**App status** (`app-status.json`): ephemeral state — paths, focused pane, view modes, last-used paths per volume
@@ -83,6 +217,18 @@ This avoids serializing 50k+ entries. Virtual scrolling renders only ~50 visible
83
217
84
218
Philosophy: status is "where you are" (ephemeral), settings are "how you like it" (preferences).
85
219
220
+
**Persistence timing** (what's at risk on crash):
221
+
222
+
| State | Timing | Crash loss |
223
+
|-------|--------|------------|
224
+
| Pane paths, focused pane, view mode, sort | Debounced 200ms (`saveAppStatus`) | Up to 200ms of changes |
225
+
| Tab state (paths, sort, viewMode, pinned) |**Immediate** (no debounce) | None — tabs are the reliable source of truth |
226
+
|`lastUsedPath` per volume |**Immediate** (no debounce) | None |
227
+
| Settings v2 | Debounced 500ms; explicit flush on Settings window close | Up to 500ms if main window crashes |
228
+
| Shortcuts |**Immediate** (changes are rare user actions) | None |
0 commit comments