mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
* feat(snapshot): add session snapshot persistence and restoration - Add snapshot middleware to persist UI state (tabs, workspaces, environments) - Add SnapshotManager service in electron for atomic snapshot storage - Add accessor-based tab serialization using pathname for reliable restoration - Add loading states for tabs while collections are mounting - Add hydrateTabs to restore tabs from snapshots on app load - Add devTools state persistence (console open/height/tab) * fix(snapshot): preserve unloaded collections and fix async serialization Make serializeSnapshot async to fetch existing snapshot before saving, ensuring collections not currently loaded in Redux are preserved. Fix activeTab serialization to pass collection object instead of just UID. * refactor(snapshot): rewrite storage to map-based schema with granular IPC Replace array-based snapshot storage with key-value maps keyed by pathname for O(1) lookups. Separate tabs into their own top-level map, decoupled from collections. - Rewrite SnapshotManager with map-based schema and granular read/write methods (getWorkspace, getTabs, setCollection, removeWorkspace, etc.) - Add 12 granular IPC handlers (renderer:snapshot:*) replacing 4 coarse ones - Update middleware serialization to produce maps; remove activeCollectionUidCache in favor of lastActiveCollectionPathname on workspace objects - Fix mountCollection passing collectionUid instead of collection to restoreTabs - Preserve non-active workspace state from existing snapshot on save * wip * refactor: allow migration of old snapshot * refactor: trim down redundancy * fix: for workspace state * feat: fix for finalised schema * fix: schema cleanup * chore: simplify * chore: wait on hydration to finish * chore: snapshot changes to schema * chore: fix typo * fix: switch schema for saving and restoring extras * chore: correctness changes * chore: add active in the schema check * chore: fix lint * chore: comments * chore: make writes async * fix: sorting and cross workspace shared collection fixes * chore: add version * fix: wait on hydration * chore: fix backward compat for ui-snapshots * chore: dead code removal * Fix optional chaining in snapshot lookup --------- Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
384 lines
14 KiB
JavaScript
384 lines
14 KiB
JavaScript
import { useEffect } from 'react';
|
|
import {
|
|
updateCookies,
|
|
updatePreferences,
|
|
setGitVersion
|
|
} from 'providers/ReduxStore/slices/app';
|
|
import {
|
|
addTab
|
|
} from 'providers/ReduxStore/slices/tabs';
|
|
import {
|
|
brunoConfigUpdateEvent,
|
|
collectionAddDirectoryEvent,
|
|
collectionAddFileEvent,
|
|
collectionChangeFileEvent,
|
|
collectionRenamedEvent,
|
|
collectionUnlinkDirectoryEvent,
|
|
collectionUnlinkEnvFileEvent,
|
|
collectionUnlinkFileEvent,
|
|
processEnvUpdateEvent,
|
|
workspaceEnvUpdateEvent,
|
|
requestCancelled,
|
|
runFolderEvent,
|
|
runRequestEvent,
|
|
scriptEnvironmentUpdateEvent,
|
|
streamDataReceived,
|
|
setDotEnvVariables
|
|
} from 'providers/ReduxStore/slices/collections';
|
|
import { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionWithUiStateSnapshot, mergeAndPersistEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
|
import {
|
|
workspaceOpenedEvent,
|
|
workspaceConfigUpdatedEvent,
|
|
hydrateSnapshotForOpenedCollection
|
|
} from 'providers/ReduxStore/slices/workspaces/actions';
|
|
import { workspaceDotEnvUpdateEvent, setWorkspaceDotEnvVariables } from 'providers/ReduxStore/slices/workspaces';
|
|
import toast from 'react-hot-toast';
|
|
import { useDispatch, useStore } from 'react-redux';
|
|
import { isElectron } from 'utils/common/platform';
|
|
import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments';
|
|
import { collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByCredentialsId, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index';
|
|
import { addLog } from 'providers/ReduxStore/slices/logs';
|
|
import { updateSystemResources } from 'providers/ReduxStore/slices/performance';
|
|
import { apiSpecAddFileEvent, apiSpecChangeFileEvent } from 'providers/ReduxStore/slices/apiSpec';
|
|
|
|
const useIpcEvents = () => {
|
|
const dispatch = useDispatch();
|
|
const store = useStore();
|
|
|
|
useEffect(() => {
|
|
if (!isElectron()) {
|
|
return () => {};
|
|
}
|
|
|
|
const { ipcRenderer } = window;
|
|
|
|
const _collectionTreeUpdated = (type, val) => {
|
|
if (window.__IS_DEV__) {
|
|
console.log(type);
|
|
console.log(val);
|
|
}
|
|
if (type === 'addDir') {
|
|
dispatch(
|
|
collectionAddDirectoryEvent({
|
|
dir: val
|
|
})
|
|
);
|
|
}
|
|
if (type === 'addFile') {
|
|
dispatch(
|
|
collectionAddFileEvent({
|
|
file: val
|
|
})
|
|
);
|
|
}
|
|
if (type === 'change') {
|
|
dispatch(
|
|
collectionChangeFileEvent({
|
|
file: val
|
|
})
|
|
);
|
|
}
|
|
if (type === 'unlink') {
|
|
setTimeout(() => {
|
|
dispatch(
|
|
collectionUnlinkFileEvent({
|
|
file: val
|
|
})
|
|
);
|
|
}, 100);
|
|
}
|
|
if (type === 'unlinkDir') {
|
|
dispatch(
|
|
collectionUnlinkDirectoryEvent({
|
|
directory: val
|
|
})
|
|
);
|
|
}
|
|
if (type === 'addEnvironmentFile') {
|
|
dispatch(collectionAddEnvFileEvent(val));
|
|
}
|
|
if (type === 'unlinkEnvironmentFile') {
|
|
dispatch(collectionUnlinkEnvFileEvent(val));
|
|
}
|
|
};
|
|
|
|
const _apiSpecTreeUpdated = (type, val) => {
|
|
if (window.__IS_DEV__) {
|
|
console.log('API Spec update:', type);
|
|
console.log(val);
|
|
}
|
|
if (type === 'addFile') {
|
|
dispatch(apiSpecAddFileEvent({ data: val }));
|
|
}
|
|
if (type === 'changeFile') {
|
|
dispatch(apiSpecChangeFileEvent({ data: val }));
|
|
}
|
|
};
|
|
|
|
ipcRenderer.invoke('renderer:ready');
|
|
|
|
const removeCollectionTreeUpdateListener = ipcRenderer.on('main:collection-tree-updated', _collectionTreeUpdated);
|
|
|
|
const removeApiSpecTreeUpdateListener = ipcRenderer.on('main:apispec-tree-updated', _apiSpecTreeUpdated);
|
|
|
|
const removeOpenCollectionListener = ipcRenderer.on('main:collection-opened', async (pathname, uid, brunoConfig) => {
|
|
try {
|
|
await dispatch(openCollectionEvent(uid, pathname, brunoConfig));
|
|
} finally {
|
|
dispatch(hydrateSnapshotForOpenedCollection(pathname));
|
|
}
|
|
});
|
|
|
|
const removeOpenWorkspaceListener = ipcRenderer.on('main:workspace-opened', (workspacePath, workspaceUid, workspaceConfig) => {
|
|
dispatch(workspaceOpenedEvent(workspacePath, workspaceUid, workspaceConfig));
|
|
});
|
|
|
|
const removeWorkspaceConfigUpdatedListener = ipcRenderer.on('main:workspace-config-updated', (workspacePath, workspaceUid, workspaceConfig) => {
|
|
dispatch(workspaceConfigUpdatedEvent(workspacePath, workspaceUid, workspaceConfig));
|
|
});
|
|
|
|
const removeWorkspaceEnvironmentAddedListener = ipcRenderer.on('main:workspace-environment-added', (workspaceUid, file) => {
|
|
const state = store.getState();
|
|
const activeWorkspaceUid = state.workspaces?.activeWorkspaceUid;
|
|
if (activeWorkspaceUid === workspaceUid) {
|
|
const workspace = state.workspaces?.workspaces?.find((w) => w.uid === workspaceUid);
|
|
if (workspace) {
|
|
ipcRenderer.invoke('renderer:get-global-environments', {
|
|
workspaceUid,
|
|
workspacePath: workspace.pathname
|
|
}).then((result) => {
|
|
dispatch(updateGlobalEnvironments(result));
|
|
}).catch((error) => {
|
|
console.error('Error refreshing global environments:', error);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
const removeWorkspaceEnvironmentChangedListener = ipcRenderer.on('main:workspace-environment-changed', (workspaceUid, file) => {
|
|
const state = store.getState();
|
|
const activeWorkspaceUid = state.workspaces?.activeWorkspaceUid;
|
|
if (activeWorkspaceUid === workspaceUid) {
|
|
const workspace = state.workspaces?.workspaces?.find((w) => w.uid === workspaceUid);
|
|
if (workspace) {
|
|
ipcRenderer.invoke('renderer:get-global-environments', {
|
|
workspaceUid,
|
|
workspacePath: workspace.pathname
|
|
}).then((result) => {
|
|
dispatch(updateGlobalEnvironments(result));
|
|
}).catch((error) => {
|
|
console.error('Error refreshing global environments:', error);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
const removeWorkspaceEnvironmentDeletedListener = ipcRenderer.on('main:workspace-environment-deleted', (workspaceUid, environmentUid) => {
|
|
const state = store.getState();
|
|
const activeWorkspaceUid = state.workspaces?.activeWorkspaceUid;
|
|
if (activeWorkspaceUid === workspaceUid) {
|
|
const workspace = state.workspaces?.workspaces?.find((w) => w.uid === workspaceUid);
|
|
if (workspace) {
|
|
ipcRenderer.invoke('renderer:get-global-environments', {
|
|
workspaceUid,
|
|
workspacePath: workspace.pathname
|
|
}).then((result) => {
|
|
dispatch(updateGlobalEnvironments(result));
|
|
}).catch((error) => {
|
|
console.error('Error refreshing global environments:', error);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
const removeDisplayErrorListener = ipcRenderer.on('main:display-error', (error) => {
|
|
if (typeof error === 'string') {
|
|
return toast.error(error || 'Something went wrong!');
|
|
}
|
|
if (typeof error === 'object') {
|
|
return toast.error(error.message || 'Something went wrong!');
|
|
}
|
|
});
|
|
|
|
const removeScriptEnvUpdateListener = ipcRenderer.on('main:script-environment-update', (val) => {
|
|
dispatch(scriptEnvironmentUpdateEvent(val));
|
|
});
|
|
|
|
const removePersistentEnvVariablesUpdateListener = ipcRenderer.on('main:persistent-env-variables-update', (val) => {
|
|
dispatch(mergeAndPersistEnvironment(val));
|
|
});
|
|
|
|
const removeGlobalEnvironmentVariablesUpdateListener = ipcRenderer.on('main:global-environment-variables-update', (val) => {
|
|
dispatch(globalEnvironmentsUpdateEvent(val));
|
|
});
|
|
|
|
const removeCollectionRenamedListener = ipcRenderer.on('main:collection-renamed', (val) => {
|
|
dispatch(collectionRenamedEvent(val));
|
|
});
|
|
|
|
const removeRunFolderEventListener = ipcRenderer.on('main:run-folder-event', (val) => {
|
|
dispatch(runFolderEvent(val));
|
|
});
|
|
|
|
const removeRunRequestEventListener = ipcRenderer.on('main:run-request-event', (val) => {
|
|
dispatch(runRequestEvent(val));
|
|
});
|
|
|
|
const removeProcessEnvUpdatesListener = ipcRenderer.on('main:process-env-update', (val) => {
|
|
dispatch(processEnvUpdateEvent(val));
|
|
});
|
|
|
|
const removeWorkspaceDotEnvUpdatesListener = ipcRenderer.on('main:workspace-dotenv-update', (val) => {
|
|
dispatch(workspaceDotEnvUpdateEvent(val));
|
|
dispatch(workspaceEnvUpdateEvent({ processEnvVariables: val.processEnvVariables }));
|
|
});
|
|
|
|
const removeDotEnvFileUpdateListener = ipcRenderer.on('main:dotenv-file-update', (val) => {
|
|
const { type, collectionUid, workspaceUid, filename, variables, exists, processEnvVariables } = val;
|
|
|
|
if (type === 'collection' && collectionUid) {
|
|
dispatch(setDotEnvVariables({
|
|
collectionUid,
|
|
variables,
|
|
exists,
|
|
filename
|
|
}));
|
|
if (filename === '.env') {
|
|
dispatch(processEnvUpdateEvent({ collectionUid, processEnvVariables }));
|
|
}
|
|
} else if (type === 'workspace' && workspaceUid) {
|
|
dispatch(setWorkspaceDotEnvVariables({
|
|
workspaceUid,
|
|
variables,
|
|
exists,
|
|
filename
|
|
}));
|
|
if (filename === '.env') {
|
|
dispatch(workspaceDotEnvUpdateEvent(val));
|
|
dispatch(workspaceEnvUpdateEvent({ processEnvVariables }));
|
|
}
|
|
}
|
|
});
|
|
|
|
const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => {
|
|
console[val.type](...val.args);
|
|
dispatch(addLog({
|
|
type: val.type,
|
|
args: val.args,
|
|
timestamp: new Date().toISOString()
|
|
}));
|
|
});
|
|
|
|
const removeSystemResourcesListener = ipcRenderer.on('main:filesync-system-resources', (resourceData) => {
|
|
dispatch(updateSystemResources(resourceData));
|
|
});
|
|
|
|
const removeConfigUpdatesListener = ipcRenderer.on('main:bruno-config-update', (val) =>
|
|
dispatch(brunoConfigUpdateEvent(val))
|
|
);
|
|
|
|
const removeShowPreferencesListener = ipcRenderer.on('main:open-preferences', () => {
|
|
const state = store.getState();
|
|
const activeWorkspaceUid = state.workspaces?.activeWorkspaceUid;
|
|
const workspaces = state.workspaces?.workspaces;
|
|
const tabs = state.tabs?.tabs;
|
|
const activeTabUid = state.tabs?.activeTabUid;
|
|
const activeTab = tabs?.find((t) => t.uid === activeTabUid);
|
|
|
|
const activeWorkspace = workspaces?.find((w) => w.uid === activeWorkspaceUid);
|
|
const collectionUid = activeTab?.collectionUid || activeWorkspace?.scratchCollectionUid;
|
|
|
|
dispatch(
|
|
addTab({
|
|
type: 'preferences',
|
|
uid: collectionUid ? `${collectionUid}-preferences` : 'preferences',
|
|
collectionUid
|
|
})
|
|
);
|
|
});
|
|
|
|
const removePreferencesUpdatesListener = ipcRenderer.on('main:load-preferences', (val) => {
|
|
dispatch(updatePreferences(val));
|
|
});
|
|
|
|
const removeCookieUpdateListener = ipcRenderer.on('main:cookies-update', (val) => {
|
|
dispatch(updateCookies(val));
|
|
});
|
|
|
|
const removeGlobalEnvironmentsUpdatesListener = ipcRenderer.on('main:load-global-environments', (val) => {
|
|
dispatch(updateGlobalEnvironments(val));
|
|
});
|
|
|
|
const removeSnapshotHydrationListener = ipcRenderer.on('main:hydrate-app-with-ui-state-snapshot', (val) => {
|
|
dispatch(hydrateCollectionWithUiStateSnapshot(val));
|
|
});
|
|
|
|
const removeCollectionOauth2CredentialsUpdatesListener = ipcRenderer.on('main:credentials-update', (val) => {
|
|
const payload = {
|
|
...val,
|
|
itemUid: val.itemUid || null,
|
|
folderUid: val.folderUid || null,
|
|
credentialsId: val.credentialsId || 'credentials'
|
|
};
|
|
dispatch(collectionAddOauth2CredentialsByUrl(payload));
|
|
});
|
|
|
|
const removeCollectionOauth2CredentialsClearListener = ipcRenderer.on('main:credentials-clear', (val) => {
|
|
dispatch(collectionClearOauth2CredentialsByCredentialsId(val));
|
|
});
|
|
|
|
const removeHttpStreamNewDataListener = ipcRenderer.on('main:http-stream-new-data', (val) => {
|
|
dispatch(streamDataReceived(val));
|
|
});
|
|
|
|
const removeHttpStreamEndListener = ipcRenderer.on('main:http-stream-end', (val) => {
|
|
dispatch(requestCancelled(val));
|
|
});
|
|
|
|
const removeCollectionLoadingStateListener = ipcRenderer.on('main:collection-loading-state-updated', (val) => {
|
|
dispatch(updateCollectionLoadingState(val));
|
|
});
|
|
|
|
const gitVersionListener = ipcRenderer.on('main:git-version', (val) => {
|
|
dispatch(setGitVersion(val));
|
|
});
|
|
|
|
return () => {
|
|
removeCollectionTreeUpdateListener();
|
|
removeApiSpecTreeUpdateListener();
|
|
removeOpenCollectionListener();
|
|
removeOpenWorkspaceListener();
|
|
removeWorkspaceConfigUpdatedListener();
|
|
removeWorkspaceEnvironmentAddedListener();
|
|
removeWorkspaceEnvironmentChangedListener();
|
|
removeWorkspaceEnvironmentDeletedListener();
|
|
removeDisplayErrorListener();
|
|
removeScriptEnvUpdateListener();
|
|
removeGlobalEnvironmentVariablesUpdateListener();
|
|
removeCollectionRenamedListener();
|
|
removeRunFolderEventListener();
|
|
removeRunRequestEventListener();
|
|
removeProcessEnvUpdatesListener();
|
|
removeWorkspaceDotEnvUpdatesListener();
|
|
removeDotEnvFileUpdateListener();
|
|
removeConsoleLogListener();
|
|
removeConfigUpdatesListener();
|
|
removeShowPreferencesListener();
|
|
removePreferencesUpdatesListener();
|
|
removeCookieUpdateListener();
|
|
removeGlobalEnvironmentsUpdatesListener();
|
|
removeSnapshotHydrationListener();
|
|
removeCollectionOauth2CredentialsUpdatesListener();
|
|
removeCollectionOauth2CredentialsClearListener();
|
|
removeHttpStreamNewDataListener();
|
|
removeHttpStreamEndListener();
|
|
removeCollectionLoadingStateListener();
|
|
removePersistentEnvVariablesUpdateListener();
|
|
removeSystemResourcesListener();
|
|
gitVersionListener();
|
|
};
|
|
}, [isElectron]);
|
|
};
|
|
|
|
export default useIpcEvents;
|