Skip to content

Commit 172b5f0

Browse files
fix: refresh button no longer crashes on large vaults (v3.13.3)
Always use folder-scoped refresh from the button, even at vault root, instead of falling back to the heavy full-vault loadFiles(). Replace flushSync with safe async yield to prevent React render crashes. Add concurrency guard to prevent overlapping refresh operations. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b75d378 commit 172b5f0

File tree

4 files changed

+27
-15
lines changed

4 files changed

+27
-15
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
All notable changes to BluePLM will be documented in this file.
44

5+
## [3.13.3] - 2026-02-09
6+
7+
### Fixed
8+
- **Refresh button no longer crashes on large vaults**: The file browser refresh button at vault root was falling back to `loadFiles()` — a heavy full-vault reload that blocks the main process and can freeze or crash the app on large vaults (25k+ files). The button now always uses the lightweight folder-scoped refresh, which works correctly at root too
9+
- **Refresh button no longer crashes React**: The refresh handler used `flushSync` to force a synchronous render, which throws if called during an existing React render cycle (e.g., rapid clicks or concurrent transitions). Replaced with a safe async yield to the UI thread
10+
- **Rapid refresh clicks no longer corrupt file list**: Added a concurrency guard so overlapping refresh operations are skipped instead of running in parallel and corrupting state
11+
12+
---
13+
514
## [3.13.2] - 2026-02-05
615

716
### Improved

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "blue-plm",
3-
"version": "3.13.2",
3+
"version": "3.13.3",
44
"description": "Open-source Product Lifecycle Management",
55
"main": "dist-electron/main.js",
66
"scripts": {

src/features/source/browser/FilePane.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,12 +1376,12 @@ export function FilePane({ onRefresh, onRefreshFolder }: FilePaneProps) {
13761376
onRefresh={() => {
13771377
logExplorer('FilePane onRefresh', {
13781378
currentPath: currentPath || '(root)',
1379-
useFolderRefresh: !!(currentPath && onRefreshFolder)
1379+
useFolderRefresh: !!onRefreshFolder
13801380
})
1381-
// Use folder-scoped refresh when in a subfolder (faster)
1382-
// Fall back to full refresh at root or if folder refresh unavailable
1383-
if (currentPath && onRefreshFolder) {
1384-
onRefreshFolder(currentPath)
1381+
// Always use folder-scoped refresh (works at root with empty string too)
1382+
// Never fall back to loadFiles() which is too heavy for a refresh button
1383+
if (onRefreshFolder) {
1384+
onRefreshFolder(currentPath || '')
13851385
} else {
13861386
onRefresh()
13871387
}

src/hooks/useLoadFiles.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, startTransition } from 'react'
2-
import { flushSync } from 'react-dom'
2+
// flushSync removed - causes React crashes when called during existing render cycles
33
import { usePDMStore } from '@/stores/pdmStore'
44
import { getFilesLightweight, getCheckedOutUsers, getVaultFolders } from '@/lib/supabase'
55
import { executeCommand } from '@/lib/commands'
@@ -1301,14 +1301,17 @@ export function useLoadFiles() {
13011301
window.electronAPI?.log('info', '[RefreshFolder] Called with', { folderPath, vaultPath })
13021302
if (!window.electronAPI || !vaultPath) return
13031303

1304-
// Force React to render loading state immediately before heavy work
1305-
// flushSync ensures the spinner is visible before the IPC call is made
1306-
logExplorer('refreshCurrentFolder BEFORE flushSync')
1307-
flushSync(() => {
1308-
setIsLoading(true)
1309-
setStatusMessage(`Refreshing folder...`)
1310-
})
1311-
logExplorer('refreshCurrentFolder AFTER flushSync')
1304+
// Guard against concurrent refreshes
1305+
if (usePDMStore.getState().isLoading) {
1306+
window.electronAPI?.log('info', '[RefreshFolder] Skipping - already refreshing')
1307+
return
1308+
}
1309+
1310+
// Set loading state and yield to UI thread so spinner renders before heavy work
1311+
// (replaces flushSync which can crash React if called during an existing render cycle)
1312+
setIsLoading(true)
1313+
setStatusMessage(`Refreshing folder...`)
1314+
await new Promise(resolve => setTimeout(resolve, 0))
13121315

13131316
try {
13141317
// 1. Get existing files from store

0 commit comments

Comments
 (0)