feat: error-boundary + crash cache addition (#8056)

* internal emit chain for clearance

* fix: crash ui

* fix(ErrorBoundary): ensure cache clearing is awaited before force quitting

* test(e2e): environment persistence across collections

* test: migration test

* Update environment.spec.ts

* fix: reduce padding for dark mode app errors

* chore: re-add waitForReadyPage
This commit is contained in:
Sid
2026-05-20 22:25:06 +05:30
committed by GitHub
parent cdba12387e
commit 659e02ac44
13 changed files with 371 additions and 7 deletions

View File

@@ -6,7 +6,7 @@ class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
this.state = { hasError: false, clearCaches: false };
}
componentDidMount() {
@@ -21,6 +21,10 @@ class ErrorBoundary extends React.Component {
this.setState({ hasError: true, error, errorInfo });
}
async clearCache() {
await window.ipcRenderer.invoke('main:cache-clear');
}
returnToApp() {
const { ipcRenderer } = window;
ipcRenderer.invoke('open-file');
@@ -36,7 +40,7 @@ class ErrorBoundary extends React.Component {
render() {
if (this.state.hasError) {
return (
<div className="flex text-center justify-center p-20 h-full">
<div className="flex text-center justify-center p-10 h-full">
<div className="bg-white rounded-lg p-10 w-full">
<div className="m-auto" style={{ width: '256px' }}>
<Bruno width={256} />
@@ -63,8 +67,30 @@ class ErrorBoundary extends React.Component {
Return to App
</button>
<div className="text-red-500 mt-3">
<a href="" className="hover:underline cursor-pointer" onClick={this.forceQuit}>
<div className="mt-5 pt-4 border-t border-gray-100 flex flex-col items-center gap-2">
<label className="flex items-center gap-2 text-sm text-gray-600 cursor-pointer select-none hover:text-gray-800 transition">
<input
type="checkbox"
checked={this.state.clearCaches}
onChange={(e) => this.setState({ clearCaches: e.target.checked })}
className="cursor-pointer"
/>
Clear caches on quit
</label>
<a
href=""
className="text-sm text-red-400 border border-red-400 hover:text-red-600 px-4 py-2 rounded transition cursor-pointer"
onClick={async (e) => {
e.preventDefault();
try {
if (this.state.clearCaches) {
await this.clearCache();
}
} finally {
this.forceQuit();
}
}}
>
Force Quit
</a>
</div>

View File

@@ -157,6 +157,9 @@ const serializeSnapshot = async (state) => {
const workspacePathname = activeWorkspace?.pathname || '';
const collectionSnapshotKey = getWorkspaceCollectionSnapshotKey(workspacePathname, collection.pathname);
const existingCollection = (collectionSnapshotKey && existingSnapshotLookups.collectionsByWorkspaceAndPath?.[collectionSnapshotKey])
|| existingSnapshotLookups.collectionsByPath?.[normalizedPath]
|| null;
if (collectionSnapshotKey) {
serializedCollectionKeys.add(collectionSnapshotKey);
}
@@ -174,7 +177,17 @@ const serializeSnapshot = async (state) => {
);
const selectedEnvironment = (collection.environments || []).find((env) => env.uid === collection.activeEnvironmentUid);
const environmentPath = getCollectionEnvironmentPath(collection, selectedEnvironment, '');
const environmentPathFromRedux = getCollectionEnvironmentPath(collection, selectedEnvironment, '');
const selectedEnvironmentFromRedux = selectedEnvironment?.name || '';
const existingEnvironmentPath = existingCollection?.environment?.collection || existingCollection?.environmentPath || '';
const existingSelectedEnvironment = existingCollection?.selectedEnvironment || '';
const shouldPreserveExistingEnvironment = collection.mountStatus !== 'mounted'
&& !environmentPathFromRedux
&& !selectedEnvironmentFromRedux;
const environmentPath = shouldPreserveExistingEnvironment ? existingEnvironmentPath : environmentPathFromRedux;
const selectedEnvironmentName = shouldPreserveExistingEnvironment
? existingSelectedEnvironment
: selectedEnvironmentFromRedux;
snapshot.collections.push({
pathname: collection.pathname,
@@ -184,7 +197,7 @@ const serializeSnapshot = async (state) => {
global: globalEnvironments.activeGlobalEnvironmentUid || ''
},
environmentPath,
selectedEnvironment: selectedEnvironment?.name || '',
selectedEnvironment: selectedEnvironmentName,
isOpen: !collection.collapsed,
isMounted: collection.mountStatus === 'mounted',
activeTab: serializeActiveTab(activeTabInCollection, collection),

View File

@@ -471,6 +471,11 @@ app.on('ready', async () => {
registerSystemMonitorIpc(mainWindow, systemMonitor);
registerGitIpc(mainWindow);
registerOpenAPISyncIpc(mainWindow);
// Internal delegator
ipcMain.handle('main:cache-clear', async () => {
ipcMain.emit('internal:snapshot:reset');
});
});
// Quit the app once all windows are closed

View File

@@ -10,6 +10,14 @@ const registerSnapshotIpc = () => {
return snapshotManager.getTabs(collectionPathname, workspacePathname);
});
ipcMain.on('internal:snapshot:reset', () => {
try {
snapshotManager.resetSnapshot();
} catch (err) {
// digest error if reset fails
}
});
ipcMain.handle('renderer:snapshot:save', async (event, data) => {
return snapshotManager.saveSnapshot(data);
});

View File

@@ -187,6 +187,23 @@ class SnapshotManager {
}
}
resetSnapshot() {
this.store.delete('activeWorkspacePath');
this.store.set('workspaces', (this.store.store?.workspaces ?? []).map((d) => {
d.lastActiveCollectionPathname = undefined;
return d;
}));
this.store.set('collections', (this.store.store?.collections ?? []).map((d) => {
if ('tabs' in d) {
d.tabs = [];
}
if ('activeTab' in d) {
d.activeTab = undefined;
}
return d;
}));
}
setCollection(pathname, data) {
const normalizedPath = normalizeLookupKey(pathname);
if (!normalizedPath) {