Skip to content

Commit 683d487

Browse files
authored
feat: faster collection mount via file cache (Beta) (usebruno#8222)
1 parent d1ebf57 commit 683d487

24 files changed

Lines changed: 1559 additions & 236 deletions

File tree

package-lock.json

Lines changed: 119 additions & 94 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/bruno-app/src/components/Preferences/Cache/StyledWrapper.js

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,85 @@ import styled from 'styled-components';
22

33
const StyledWrapper = styled.div`
44
color: ${(props) => props.theme.text};
5-
6-
form.bruno-form {
7-
label {
8-
font-size: 0.8125rem;
9-
}
5+
6+
.cache-section-title {
7+
text-transform: uppercase;
8+
font-size: ${(props) => props.theme.font.size.sm};
9+
letter-spacing: 0.05em;
10+
color: ${(props) => props.theme.colors.text.muted};
11+
margin-bottom: 0.75rem;
12+
}
13+
14+
.cache-item {
15+
border: 1px solid ${(props) => props.theme.border.border1};
16+
border-radius: ${(props) => props.theme.border.radius.md};
17+
margin-bottom: 1rem;
18+
overflow: hidden;
19+
}
20+
21+
.cache-item-header {
22+
display: flex;
23+
align-items: center;
24+
justify-content: space-between;
25+
padding: 0.75rem 1rem;
26+
gap: 1rem;
27+
background: ${(props) => props.theme.background.surface0};
28+
border-bottom: 1px solid ${(props) => props.theme.border.border1};
29+
}
30+
31+
.cache-item-title-group {
32+
display: flex;
33+
align-items: center;
34+
gap: 0.5rem;
35+
}
36+
37+
.cache-item-title {
38+
font-size: ${(props) => props.theme.font.size.md};
39+
font-weight: 600;
40+
}
41+
42+
.beta-badge {
43+
display: inline-flex;
44+
align-items: center;
45+
padding: 1px 6px;
46+
border-radius: ${(props) => props.theme.border.radius.sm};
47+
font-size: ${(props) => props.theme.font.size.xs};
48+
font-weight: 500;
49+
line-height: 1.5;
50+
background: ${(props) => props.theme.status.info.background};
51+
color: ${(props) => props.theme.status.info.text};
52+
}
53+
54+
.cache-item-body {
55+
display: flex;
56+
align-items: flex-end;
57+
justify-content: space-between;
58+
padding: 0.875rem 1rem;
59+
gap: 1.25rem;
60+
}
61+
62+
.cache-item-body-text {
63+
flex: 1;
64+
min-width: 0;
65+
}
66+
67+
.cache-item-description {
68+
font-size: ${(props) => props.theme.font.size.base};
69+
color: ${(props) => props.theme.colors.text.muted};
70+
line-height: 1.5;
71+
margin: 0;
72+
}
73+
74+
.cache-item-size {
75+
font-size: ${(props) => props.theme.font.size.base};
76+
color: ${(props) => props.theme.colors.text.subtext2};
77+
margin: 0.5rem 0 0 0;
78+
}
79+
80+
.cache-item-size strong {
81+
font-weight: 600;
82+
color: ${(props) => props.theme.text};
83+
margin-left: 0.25rem;
1084
}
1185
`;
1286

packages/bruno-app/src/components/Preferences/Cache/index.js

Lines changed: 116 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,147 @@
1-
import React, { useEffect, useCallback, useRef } from 'react';
2-
import { useFormik } from 'formik';
1+
import React, { useEffect, useState, useCallback } from 'react';
32
import { useSelector, useDispatch } from 'react-redux';
4-
import {
5-
savePreferences,
6-
clearHttpHttpsAgentCache
7-
} from 'providers/ReduxStore/slices/app';
3+
import { savePreferences, clearHttpHttpsAgentCache } from 'providers/ReduxStore/slices/app';
84
import toast from 'react-hot-toast';
9-
import StyledWrapper from './StyledWrapper';
10-
import * as Yup from 'yup';
11-
import debounce from 'lodash/debounce';
125
import get from 'lodash/get';
13-
14-
const cacheSchema = Yup.object().shape({
15-
sslSession: Yup.object({
16-
enabled: Yup.boolean()
17-
})
18-
});
6+
import { IconEraser } from '@tabler/icons';
7+
import { useTheme } from 'providers/Theme';
8+
import ToggleSwitch from 'components/ToggleSwitch';
9+
import ActionIcon from 'ui/ActionIcon';
10+
import StyledWrapper from './StyledWrapper';
11+
import { formatSize } from 'utils/common';
1912

2013
const Cache = () => {
2114
const preferences = useSelector((state) => state.app.preferences);
2215
const dispatch = useDispatch();
16+
const { theme } = useTheme();
17+
const { ipcRenderer } = window;
2318

24-
const handleSave = useCallback(
25-
(newCachePreferences) => {
26-
dispatch(
27-
savePreferences({
28-
...preferences,
29-
cache: newCachePreferences
30-
})
31-
).catch(() => toast.error('Failed to update cache preferences'));
32-
},
33-
[dispatch, preferences]
34-
);
35-
36-
const handleSaveRef = useRef(handleSave);
37-
handleSaveRef.current = handleSave;
19+
const fileCacheEnabled = get(preferences, 'cache.file.enabled', false);
20+
const sslSessionEnabled = get(preferences, 'cache.sslSession.enabled', false);
3821

39-
const formik = useFormik({
40-
initialValues: {
41-
sslSession: {
42-
enabled: get(preferences, 'cache.sslSession.enabled', false)
43-
}
44-
},
45-
validationSchema: cacheSchema,
46-
onSubmit: async (values) => {
47-
try {
48-
const newPreferences = await cacheSchema.validate(values, { abortEarly: true });
49-
handleSave(newPreferences);
50-
} catch (error) {
51-
console.error('Cache preferences validation error:', error.message);
52-
}
53-
}
54-
});
22+
const [fileCacheSize, setFileCacheSize] = useState(null);
5523

56-
const debouncedSave = useCallback(
57-
debounce((values) => {
58-
cacheSchema
59-
.validate(values, { abortEarly: true })
60-
.then((validatedValues) => handleSaveRef.current(validatedValues))
61-
.catch(() => {});
62-
}, 500),
63-
[]
64-
);
24+
const refreshFileCacheSize = useCallback(() => {
25+
if (!ipcRenderer) return;
26+
ipcRenderer
27+
.invoke('renderer:get-file-cache-size')
28+
.then((size) => setFileCacheSize(size))
29+
.catch(() => setFileCacheSize(null));
30+
}, [ipcRenderer]);
6531

6632
useEffect(() => {
67-
if (formik.dirty && formik.isValid) {
68-
debouncedSave(formik.values);
69-
}
70-
return () => {
71-
debouncedSave.flush();
72-
};
73-
}, [formik.values, formik.dirty, formik.isValid, debouncedSave]);
33+
refreshFileCacheSize();
34+
}, [refreshFileCacheSize, fileCacheEnabled]);
7435

75-
const handleAgentCachingChange = (e) => {
76-
formik.handleChange(e);
77-
// Immediately evict all cached agents when caching is disabled
78-
if (!e.target.checked) {
36+
const persist = (next) => {
37+
dispatch(savePreferences({ ...preferences, cache: next })).catch(() => {
38+
toast.error('Failed to update cache preferences');
39+
});
40+
};
41+
42+
const handleToggleFileCache = () => {
43+
persist({
44+
...preferences.cache,
45+
file: { enabled: !fileCacheEnabled }
46+
});
47+
};
48+
49+
const handleToggleSslSession = () => {
50+
const next = !sslSessionEnabled;
51+
persist({
52+
...preferences.cache,
53+
sslSession: { enabled: next }
54+
});
55+
if (!next) {
7956
dispatch(clearHttpHttpsAgentCache()).catch(() => {});
8057
}
8158
};
8259

83-
const handleResetCache = () => {
60+
const handleClearFileCache = () => {
61+
if (!ipcRenderer) return;
62+
ipcRenderer
63+
.invoke('renderer:clear-file-cache')
64+
.then((size) => {
65+
setFileCacheSize(size);
66+
toast.success('File cache cleared');
67+
})
68+
.catch(() => toast.error('Failed to clear file cache'));
69+
};
70+
71+
const handleClearSslSession = () => {
8472
dispatch(clearHttpHttpsAgentCache())
85-
.then(() => toast.success('ssl session cache cleared'))
86-
.catch(() => toast.error('Failed to clear ssl session cache'));
73+
.then(() => toast.success('SSL session cache cleared'))
74+
.catch(() => toast.error('Failed to clear SSL session cache'));
8775
};
8876

8977
return (
9078
<StyledWrapper className="w-full">
91-
<form className="bruno-form" onSubmit={formik.handleSubmit}>
92-
<div className="section-title mt-6 mb-3">Cache SSL Session</div>
79+
<div className="cache-section-title">Cache</div>
9380

94-
<div className="flex items-center my-2">
95-
<input
96-
id="sslSession.enabled"
97-
type="checkbox"
98-
name="sslSession.enabled"
99-
checked={formik.values.sslSession.enabled}
100-
onChange={handleAgentCachingChange}
101-
className="mousetrap mr-0"
81+
<div className="cache-item">
82+
<div className="cache-item-header">
83+
<div className="cache-item-title-group">
84+
<span className="cache-item-title">File cache</span>
85+
<span className="beta-badge">Beta</span>
86+
</div>
87+
<ToggleSwitch
88+
data-testid="cache.file.enabled"
89+
isOn={fileCacheEnabled}
90+
handleToggle={handleToggleFileCache}
91+
size="2xs"
92+
activeColor={theme.primary.solid}
10293
/>
103-
<label className="block ml-2 select-none" htmlFor="sslSession.enabled">
104-
Enable SSL session caching
105-
</label>
10694
</div>
107-
<div className="text-xs mt-1 ml-6 opacity-70">
108-
Reuses TLS sessions and connections across requests for faster handshakes. Disable to create a fresh connection for every
109-
request.
95+
<div className="cache-item-body">
96+
<div className="cache-item-body-text">
97+
<p className="cache-item-description">
98+
Loads your workspace faster by caching opened collections. Bruno refreshes the cache when your collection
99+
changes. Clearing it won't affect your original files.
100+
</p>
101+
<p className="cache-item-size">
102+
Cache size <strong>{fileCacheSize == null ? '—' : formatSize(fileCacheSize)}</strong>
103+
</p>
104+
</div>
105+
<ActionIcon
106+
label="Clear cache"
107+
onClick={handleClearFileCache}
108+
disabled={!fileCacheSize}
109+
colorOnHover={theme.colors.text.danger}
110+
>
111+
<IconEraser size={16} strokeWidth={1.5} />
112+
</ActionIcon>
110113
</div>
114+
</div>
111115

112-
<div className="mt-6">
113-
<button type="button" className="text-link cursor-pointer hover:underline" onClick={handleResetCache}>
114-
Clear
115-
</button>
116+
<div className="cache-item">
117+
<div className="cache-item-header">
118+
<div className="cache-item-title-group">
119+
<span className="cache-item-title">SSL session cache</span>
120+
</div>
121+
<ToggleSwitch
122+
data-testid="sslSession.enabled"
123+
isOn={sslSessionEnabled}
124+
handleToggle={handleToggleSslSession}
125+
size="2xs"
126+
activeColor={theme.primary.solid}
127+
/>
128+
</div>
129+
<div className="cache-item-body">
130+
<div className="cache-item-body-text">
131+
<p className="cache-item-description">
132+
Reuses TLS sessions and connections across requests for faster handshakes. Disable to create a fresh
133+
connection for every request.
134+
</p>
135+
</div>
136+
<ActionIcon
137+
label="Clear cache"
138+
onClick={handleClearSslSession}
139+
colorOnHover={theme.colors.text.danger}
140+
>
141+
<IconEraser size={16} strokeWidth={1.5} />
142+
</ActionIcon>
116143
</div>
117-
</form>
144+
</div>
118145
</StyledWrapper>
119146
);
120147
};

packages/bruno-app/src/providers/App/useIpcEvents.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import toast from 'react-hot-toast';
3636
import { useDispatch, useStore } from 'react-redux';
3737
import { isElectron } from 'utils/common/platform';
3838
import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments';
39-
import { collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByCredentialsId, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index';
39+
import { collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByCredentialsId, updateCollectionLoadingState, collectionLoadedFromTree } from 'providers/ReduxStore/slices/collections/index';
4040
import { addLog } from 'providers/ReduxStore/slices/logs';
4141
import { loadNotifications } from 'providers/ReduxStore/slices/notifications';
4242
import { updateSystemResources } from 'providers/ReduxStore/slices/performance';
@@ -348,7 +348,22 @@ const useIpcEvents = () => {
348348
dispatch(loadNotifications(notifications));
349349
});
350350

351+
const removeCollectionTreeLoadedListener = ipcRenderer.on('main:collection-tree-loaded', ({ collectionUid, tree }) => {
352+
dispatch(collectionLoadedFromTree({ collectionUid, tree }));
353+
});
354+
355+
const removeCollectionLoadingStateV2Listener = ipcRenderer.on('main:collection-loading-state-updated-v2', (val) => {
356+
dispatch(updateCollectionLoadingState(val));
357+
});
358+
359+
const removeBrunoConfigUpdateV2Listener = ipcRenderer.on('main:bruno-config-update-v2', (val) => {
360+
dispatch(brunoConfigUpdateEvent(val));
361+
});
362+
351363
return () => {
364+
removeCollectionTreeLoadedListener();
365+
removeCollectionLoadingStateV2Listener();
366+
removeBrunoConfigUpdateV2Listener();
352367
removeCollectionTreeUpdateListener();
353368
removeApiSpecTreeUpdateListener();
354369
removeOpenCollectionListener();

0 commit comments

Comments
 (0)