feat: add session snapshot persistence and restoration#7292
feat: add session snapshot persistence and restoration#7292chirag-bruno wants to merge 21 commits intousebruno:mainfrom
Conversation
Implements automatic session state persistence that saves and restores: - Open collections with mount status and collapsed/expanded state - Active workspace and environment selections - Open tabs with full pane state (active sub-tabs, widths, heights) - DevTools state (open/closed, active tab, height) Key features: - Lazy loading: only mounts collections that were previously mounted - Stable UIDs: uses hash-based UIDs for reliable item matching across sessions - Race condition handling: query-based mount completion tracking - Debounced saves: snapshots saved 1 second after state changes - Theme-aware loading screen with granular progress messages Files added: - AppSnapshotStore for electron-side persistence - Snapshot middleware for Redux state serialization - AppLoader component for restore progress display - Snapshot utilities for serialization/deserialization
fd4de91 to
e7f511b
Compare
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…uno#7193) * fix: skip null query parameters in Postman to Bruno conversion Updated the importPostmanV2CollectionItem function to skip query parameters where both key and value are null. Added a test case to ensure that such parameters are not included in the converted Bruno collection, while preserving other valid parameters. * fix: skip null parameters in Postman to Bruno conversion Updated the importPostmanV2CollectionItem function to skip headers, URL-encoded parameters, and form data where both key and value are null. Added corresponding test cases to ensure proper handling of these scenarios in the conversion process.
* perf: Improve search performance in code editor Added cache key to reduce duplicate searches over the complete text. Added incremental update logic, to not update every marked entry when clicking "next" or "previous". Fixes: usebruno#6913 * Update delimeter to unicode symbol
…put (usebruno#7114) * feat: add options to skip request and response bodies in reporter output - Introduced `--reporter-skip-request-body` and `--reporter-skip-response-body` flags to omit respective bodies from the reporter output. - Updated examples in the CLI documentation to reflect new options. - Refactored result sanitization to handle new flags. * feat: add shorthand option to skip both request and response bodies in reporter output - Introduced `--reporter-skip-body` as a shorthand for omitting both request and response bodies from the reporter output. - Updated CLI documentation examples to include the new shorthand option. - Adjusted result sanitization to accommodate the new option. * refactor: simplify documentation and tests for reporter-skip-body option - Updated the description of the `--reporter-skip-body` option to remove redundancy. - Removed outdated shorthand references from the test suite for clarity. - Cleaned up examples in the CLI documentation to focus on the current functionality. * fix: handle optional chaining for request and response properties in result sanitization - Updated the `sanitizeResultsForReporter` function to use optional chaining when accessing request and response headers and data. - This change prevents potential errors when these properties are undefined. * test: enhance reporter-skip-body tests for JSON and HTML outputs - Added comprehensive tests for the `--reporter-skip-request-body` and `--reporter-skip-response-body` options in both JSON and HTML report formats. - Verified that the appropriate request and response bodies are included or excluded based on the specified flags. - Improved test coverage for scenarios where both flags are used simultaneously. * fix: remove optional chaining for request and response headers in result sanitization - Updated the `sanitizeResultsForReporter` function to directly assign empty objects to request and response headers, ensuring consistent behavior regardless of their initial state. - This change simplifies the code and maintains functionality for skipping headers.
* Refactor: optimize collection updates with batch processing - Introduced BatchAggregator to handle IPC events in batches, reducing Redux dispatch overhead during collection mounting. - Updated collection watcher to utilize batch processing for adding files and directories, improving UI performance. - Implemented ParsedFileCacheStore using LMDB for efficient caching of parsed file content, enhancing loading speed and reducing redundant parsing. - Adjusted collection slice to support batch addition of items, minimizing re-renders and improving state management. - Updated relevant components to reflect changes in loading states and collection data handling. * feat: add cache management to preferences - Introduced a new Cache component in the Preferences section to display cache statistics and allow users to purge the cache. - Implemented IPC handlers for fetching cache stats and purging the cache in the Electron main process. - Added styled components for better UI presentation of cache information. - Updated Preferences component to include a new tab for cache management. * fix: update package-lock.json to change 'devOptional' to 'dev' for several Babel dependencies * refactor: update batch aggregation parameters for improved performance - Increased DISPATCH_INTERVAL_MS from 150ms to 200ms for better timing control. - Adjusted MAX_BATCH_SIZE from 200 to 300 items to enhance batch processing efficiency. * feat: enhance collection loading state and improve batch aggregator functionality - Added isLoading property to collections slice to manage loading state during collection operations. - Updated getAggregator function calls in collection-watcher to include collectionUid for better context in batch processing. - Normalized directory path handling in parsed-file-cache to ensure consistent prefix creation for cache keys. * fix: update loading state and transient file handling in collections slice - Changed isLoading property to false during collection initialization for accurate loading state representation. - Introduced isTransient flag for directories and files to differentiate between transient and non-transient items. - Enhanced logic for handling transient directories and files during collection processing to improve state management. * feat: add batch processing support for file additions in task middleware - Implemented a new listener for collectionBatchAddItems to handle batch file additions. - Enhanced task management by checking for pending OPEN_REQUEST tasks that match added files. - Improved tab management by dispatching addTab actions for matching files and removing corresponding tasks from the queue. * feat: enable ASAR packaging and unpacking for LMDB binaries in Electron build configuration - Added ASAR support to the Electron build configuration for the Bruno application. - Specified unpacking rules for LMDB native binaries to ensure proper loading during runtime. * feat: implement parsed file cache using IndexedDB for improved performance - Introduced a new `parsedFileCacheStore` utilizing IndexedDB for caching parsed file data. - Replaced the previous LMDB-based cache implementation to enhance performance and reliability. - Updated IPC handlers to manage cache operations such as get, set, invalidate, and clear. - Integrated the new cache store into various components, ensuring efficient data retrieval and storage. - Added pruning functionality to remove outdated cache entries on startup. * refactor: update collection root and item handling to preserve UIDs - Modified the way collection roots and folder items are assigned by using `mergeRootWithPreservedUids` and `mergeRequestWithPreservedUids` to ensure UIDs are maintained during updates. - This change enhances data integrity when managing collections and their associated files. * refactor: pass mainWindow reference to parsedFileCacheStore initialization - Updated the `initialize` method in `ParsedFileCacheStore` to accept a `mainWindow` parameter, allowing for direct access to the main window instance in IPC handlers. - This change improves the handling of IPC requests by ensuring the correct window context is used for sending messages. * refactor: optimize getStats method in parsedFileCacheStore for performance - Replaced manual counting of total files with a direct count() call for O(1) performance. - Updated the collection counting logic to utilize openKeyCursor with 'nextunique' for improved efficiency in counting unique collection paths. - These changes enhance the performance of the getStats method by reducing the complexity of file and collection counting. * fix: update key generation in parsedFileCache to use newline separator - Changed the key generation logic in `generateKey` from a null character to a newline character for improved readability and consistency in cache keys. * refactor: rename batch-aggregator to collection-tree-batcher and add tests - Rename BatchAggregator class to CollectionTreeBatcher - Rename getAggregator/removeAggregator to getBatcher/removeBatcher - Update imports and variable names in collection-watcher.js - Add backward-compatible aliases for old names - Add 22 unit tests covering all functionality * refactor: update key generation in parsedFileCache to use custom separator - Changed the key generation logic in `generateKey` to use a custom separator (↝) instead of a newline character for improved readability and consistency in cache keys. * fix: add missing reject handler and fix directory prefix collision - Add reject to Promise and pendingRequests in parsed-file-cache-idb.js - Normalize dirPath with trailing separator in invalidateDirectory to prevent false matches (e.g., /foo/bar matching /foo/barley) - Use platform-specific path.sep for cross-platform compatibility * fix: add error handling in parsedFileCache and update window close event - Added a catch block to handle errors in the database promise in parsedFileCache. - Updated the window close event listener in collection-tree-batcher to use `once` for better resource management. * fix: add LRU eviction when IndexedDB quota is exceeded Handle QuotaExceededError in setEntry by automatically evicting the oldest 20% of cache entries and retrying the write operation. * fix: use once instead of on in mock window for batcher tests --------- Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
…grpc local IPC support (usebruno#7021)
- Add folderUid property for folder-settings tabs in deserialization - Update restoreTabs reducer to preserve folderUid - Add folder-settings to TAB_TYPE_TO_SCHEMA mapping - Refactor waitForMountComplete to use item count stability checking - Extract createItemsLoadedChecker for reusable load state detection
Session Snapshot ArchitectureOverviewThe Session Snapshot feature provides automatic persistence and restoration of the user's application state across app restarts. This includes open tabs, active workspace, collection states, selected environments, and DevTools configuration. Design Goals
Architecture Components1. Storage Layer (Electron Main Process)File: Uses 2. IPC HandlersFile:
Snapshot Schema (Version 1){
"version": 1,
"activeWorkspacePathname": "/path/to/workspace",
"workspaces": [
{
"pathname": "/path/to/workspace",
"collections": [
{
"pathname": "/path/to/collection",
"isMounted": true,
"isOpen": true,
"environment": "production",
"tabs": [...],
"activeTabIndex": 0
}
]
}
],
"collections": [...], // Flattened for quick lookup
"extras": {
"devTools": {
"open": false,
"tab": "console",
"height": 300
}
}
}Tab SchemaTabs are serialized using relative paths to ensure portability: {
"type": "item", // "item" | "preferences" | "runner" | "variables" | etc.
"permanent": true, // Not a preview tab
"itemPath": "folder/request.bru", // Relative to collection root
"layout": "horizontal",
"request": {
"tab": "body",
"width": 0,
"height": 0
},
"response": {
"tab": "response",
"format": null,
"preview": false
}
}Tab Type Mappings
Serialization FlowFile: Key Function:
|
| Tab Type | UID Pattern | Example |
|---|---|---|
| Request/Folder | item.uid |
"a1b2c3d4" |
| Environment Settings | ${collection.uid}-environment-settings |
"xyz-environment-settings" |
| Collection Settings | ${collection.uid}-collection-settings |
"xyz-collection-settings" |
| Variables | ${collection.uid}-variables |
"xyz-variables" |
| Collection Runner | ${collection.uid}-runner |
"xyz-runner" |
| Preferences | ${collection.uid}-preferences |
"xyz-preferences" |
Restoration Flow
Startup Sequence
┌─────────────────────────────────────────────────────────────────┐
│ App Startup │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. App mounts, Redux store initializes │
│ - snapshotSaveEnabled = false (prevent saves during load) │
│ - isRestoringSnapshot = false │
│ │
│ 2. workspaceOpenedEvent fires for default workspace │
│ - Sets isInitialLoadComplete = true │
│ │
│ 3. restoreAppSnapshot() is dispatched │
│ - Sets isRestoringSnapshot = true │
│ - Shows AppLoader overlay │
│ │
│ 4. Wait for initial load complete │
│ │
│ 5. Load snapshot from main process │
│ │
│ 6. Build restore sequence: │
│ - Parse DevTools state │
│ - Identify collections to mount │
│ - Map tabs to restore per collection │
│ │
│ 7. Restore DevTools state immediately │
│ │
│ 8. Set up pending workspace restores: │
│ - For each workspace in snapshot, store its restore data │
│ - Keyed by workspace pathname │
│ │
│ 9. Switch to active workspace from snapshot │
│ - Triggers restoreWorkspaceState for that workspace │
│ │
│ 10. Enable snapshot saving │
│ - snapshotSaveEnabled = true │
│ │
└─────────────────────────────────────────────────────────────────┘
Lazy Workspace Restoration
File: packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js
┌─────────────────────────────────────────────────────────────────┐
│ switchWorkspace(workspaceUid) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Set active workspace │
│ │
│ 2. Load global environments │
│ │
│ 3. Mount scratch collection │
│ │
│ 4. Load workspace collections (lazy mount) │
│ │
│ 5. Check for pending restore: │
│ IF pendingWorkspaceRestores[workspace.pathname]: │
│ → restoreWorkspaceState(collections) │
│ → clearPendingWorkspaceRestore │
│ ELSE IF workspace not hydrated: │
│ → Show default overview tabs │
│ ELSE: │
│ → Focus existing tab from workspace │
│ │
│ 6. Mark workspace as hydrated │
│ │
└─────────────────────────────────────────────────────────────────┘
Collection Restoration
┌─────────────────────────────────────────────────────────────────┐
│ restoreWorkspaceState(collectionsToRestore) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ For each collection (parallel, max 4 concurrent): │
│ │
│ 1. Find or open collection │
│ │
│ 2. Set collapsed state (isOpen) │
│ │
│ 3. If was mounted: │
│ a. Trigger mount if not already mounted │
│ b. Wait for mount complete (with stability check) │
│ c. Select environment by name │
│ d. Restore tabs via restoreTabsForCollection │
│ │
└─────────────────────────────────────────────────────────────────┘
Mount Completion Detection
File: packages/bruno-app/src/utils/collection-mount.js
The Race Condition Problem
When a collection is mounted:
- Main process sends IPC messages for each file
- Renderer processes messages asynchronously
collection.isLoadingbecomesfalsewhen main process finishes- But renderer may still be processing messages!
Main Process: [scan files] [send IPCs] [done]
↓ isLoading = false
Renderer: [receive] [receive] [receive] [receive] ...
↑
Tab restore attempted here = RACE CONDITION
Solution: Item Count Stability
export const createItemsLoadedChecker = () => {
let lastItemCount = -1;
let stableCount = 0;
return (collection) => {
// 1. Check mount status and loading state
if (!collection.mountStatus === 'mounted' || areItemsLoading(collection)) {
return { ready: false };
}
// 2. Check item count stability
const currentItemCount = flattenItems(collection.items).length;
if (currentItemCount === lastItemCount) {
stableCount++;
if (stableCount >= 3) { // Stable for 300ms (3 × 100ms polls)
return { ready: true };
}
} else {
lastItemCount = currentItemCount;
stableCount = 1;
}
return { ready: false, reason: 'stabilizing' };
};
};This ensures all IPC messages have been processed before attempting tab restoration.
Save Triggering (Middleware)
File: packages/bruno-app/src/providers/ReduxStore/middlewares/snapshot/middleware.js
Debounced Saves
const DEBOUNCE_DELAY = 1000; // 1 second
const actionsToIntercept = [
// Tab actions
'tabs/addTab', 'tabs/focusTab', 'tabs/closeTabs', ...
// Workspace actions
'workspaces/setActiveWorkspace',
// Collection actions
'collections/selectEnvironment', 'collections/setCollectionCollapsed', ...
// DevTools actions
'logs/openConsole', 'logs/closeConsole', ...
];
// Middleware intercepts these actions and schedules a debounced saveSave Flow
Action dispatched → Middleware intercepts → Schedule save (1s debounce)
↓
Save still pending?
↓
Serialize state
↓
IPC to main process
↓
Write to disk
State Management
App Slice Additions
// packages/bruno-app/src/providers/ReduxStore/slices/app.js
initialState: {
isInitialLoadComplete: false, // True after default workspace loads
isRestoringSnapshot: false, // True during restore (shows loader)
snapshotRestoreMessage: null, // Loading message for UI
snapshotSaveEnabled: false, // False during restore to prevent saves
pendingWorkspaceRestores: {}, // Keyed by workspace pathname
}Tabs Slice Additions
// New action: restoreTabs
// Bulk adds tabs without triggering individual addTab actions
// Handles folderUid for folder-settings tabsWorkspace Hydration Tracking
// packages/bruno-app/src/utils/workspace-hydration.js
// In-memory Set tracking which workspaces have been restored
// Prevents re-applying default tabs when switching back to a workspaceUI Components
AppLoader
File: packages/bruno-app/src/components/AppLoader/index.js
Full-screen overlay shown during snapshot restoration:
- Theme-aware styling
- Animated spinner
- Dynamic message from
snapshotRestoreMessage
Edge Cases Handled
1. Deleted Files/Folders
findItemByRelativePathreturns null- Tab is not restored (silently skipped)
- Warning logged for debugging
2. Renamed Collections
- Snapshot uses absolute paths
- Collection won't be found if renamed
- Opens with default state instead
3. Multiple Workspaces
- Each workspace has its own pending restore data
- Lazy loading only restores when workspace is visited
- Prevents heavy startup load
4. Folder Tabs
- Folders require
folderUidproperty in tab restoreTabsreducer preserves this property- UI looks up folder by
focusedTab.folderUid
5. Non-Item Tabs
- Settings tabs use deterministic UIDs (collection.uid + suffix)
- Don't require item lookup
- Always restorable if collection exists
File Summary
| File | Purpose |
|---|---|
utils/app-snapshot/index.js |
Serialization functions |
utils/app-snapshot/restore.js |
Deserialization functions |
utils/collection-mount.js |
Mount completion detection |
utils/workspace-hydration.js |
Workspace visit tracking |
middlewares/snapshot/middleware.js |
Save triggering |
slices/app.js |
Snapshot state management |
slices/tabs.js |
restoreTabs action |
slices/workspaces/actions.js |
Restore orchestration |
components/AppLoader/index.js |
Loading UI |
store/app-snapshot.js (electron) |
Disk persistence |
ipc/preferences.js (electron) |
IPC handlers |
Replace action-based snapshot tracking with state-based tracking. Instead of maintaining a list of specific actions to intercept, the middleware now watches state selectors directly. This simplifies extending the snapshot to new states - just add serialization/deserialization and a selector, without needing to find all actions that mutate the state.
- Add Zod for schema validation - Create schema/v1.js with complete snapshot schema - Create schema/index.js with registry and migration runner - Validate snapshots before saving (catches serialization bugs) - Validate and migrate snapshots on load (enables version upgrades) To add a new version: create schema/vN.js, implement migrate in v(N-1).js, and add to versions array in schema/index.js.
When no tabs are open, show a friendly empty state instead of "An error occurred!" message. The EmptyState component supports two modes: - Normal: "No request open" with guidance to select a request - Error: Shows warning icon with custom error message for malformed tab states
Simplify the snapshot system by removing unnecessary abstractions,
dead code, and improving variable naming for clarity.
Key changes:
Tab UID handling:
- Make all non-item tab UIDs deterministic using ${collectionUid}-${type} pattern
- Remove uuid() calls for collection-runner, variables, openapi-sync, openapi-spec tabs
- Store tab UIDs directly in snapshot instead of deriving from suffix mappings
- Remove itemPath from schema - use uid directly (items can't be reliably restored if moved anyway)
State naming:
- Rename pendingWorkspaceRestores -> deferredWorkspaceSnapshots (clearer intent)
- Rename related actions: setPendingWorkspaceRestore -> setDeferredWorkspaceSnapshot
Code consolidation:
- Delete restore.js, move deserializeTab/deserializeDevTools/restoreTabsForCollection to index.js
- Remove buildRestoreSequence (was mostly dead code, only devTools was used)
- Remove TAB_UID_SUFFIXES mapping (no longer needed with deterministic UIDs)
- Remove getRelativeItemPath and findItemByRelativePath (no longer needed)
- Use existing isScratchCollection from utils/collections instead of local duplicate
Schema simplification:
- Remove itemPath field from tab schema
- Make uid required for all tabs
- Simplify schema validation and migration code
Variable naming improvements:
- mountedCollections -> nonScratchCollections (more accurate)
- collectionSchema -> inline destructuring
- restoreActions -> result
- pendingRestore -> deferredSnapshot
Code style:
- Use for...of instead of forEach where cleaner
- Use spread with && for conditional object properties
- Use nullish coalescing (??) instead of || where appropriate
- Inline simple expressions, reduce intermediate variables
Net reduction: ~436 lines of code
Description
Implements automatic session state persistence that saves and restores:
Key features:
Files added:
Contribution Checklist:
Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.
Publishing to New Package Managers
Please see here for more information.