|
5 | 5 | * Uses debouncing to prevent excessive disk writes. |
6 | 6 | */ |
7 | 7 |
|
8 | | -import { |
9 | | - SAVE_TRIGGERS, |
10 | | - shouldExcludeTab, |
11 | | - serializeTab, |
12 | | - serializeActiveTab, |
13 | | - getCollectionEnvironmentPath, |
14 | | - hydrateSnapshotLookups |
15 | | -} from 'utils/snapshot'; |
16 | | -import { normalizePath } from 'utils/common/path'; |
17 | | -import { TAB_IDENFIERS as DEVTOOL_TABS } from 'providers/ReduxStore/slices/logs'; |
| 8 | +import { SAVE_TRIGGERS } from 'utils/snapshot'; |
| 9 | +import { serializeSnapshot } from './serializeSnapshot'; |
18 | 10 |
|
19 | 11 | const { ipcRenderer } = window; |
20 | 12 |
|
21 | | -// Debounce timer reference |
22 | 13 | let saveTimer = null; |
23 | 14 | const DEBOUNCE_MS = 1000; |
24 | 15 |
|
25 | | -const COLLECTION_SORT_ORDER_BY_WORKSPACE_SORTING = { |
26 | | - default: 'default', |
27 | | - alphabetical: 'alphabetical', |
28 | | - reverseAlphabetical: 'reverseAlphabetical' |
29 | | -}; |
30 | | - |
31 | | -const normalizeCollectionSortOrder = (sortOrder) => { |
32 | | - return COLLECTION_SORT_ORDER_BY_WORKSPACE_SORTING[sortOrder] || 'default'; |
33 | | -}; |
34 | | - |
35 | | -const normalizeWorkspaceSorting = (sorting) => { |
36 | | - return COLLECTION_SORT_ORDER_BY_WORKSPACE_SORTING[sorting] || 'default'; |
37 | | -}; |
38 | | - |
39 | | -const getWorkspaceCollectionSnapshotKey = (workspacePathname, collectionPathname) => { |
40 | | - const normalizedCollectionPathname = normalizePath(collectionPathname); |
41 | | - if (!normalizedCollectionPathname) { |
42 | | - return ''; |
43 | | - } |
44 | | - |
45 | | - return `${normalizePath(workspacePathname || '')}::${normalizedCollectionPathname}`; |
46 | | -}; |
47 | | - |
48 | | -/** |
49 | | - * Serialize the current app state into a snapshot format |
50 | | - * Persists array-based schema and supports lookup hydration. |
51 | | - */ |
52 | | -const serializeSnapshot = async (state) => { |
53 | | - const { workspaces, collections, tabs, logs, globalEnvironments } = state; |
54 | | - const snapshotHydration = state.app?.snapshotHydration; |
55 | | - const activeWorkspaceCollectionSortOrder = normalizeCollectionSortOrder(collections.collectionSortOrder); |
56 | | - |
57 | | - // Get existing snapshot to preserve data for collections not currently loaded |
58 | | - let existingSnapshot = null; |
59 | | - try { |
60 | | - existingSnapshot = await ipcRenderer.invoke('renderer:snapshot:get'); |
61 | | - } catch (err) { |
62 | | - // Ignore - will create fresh snapshot |
63 | | - } |
64 | | - |
65 | | - const existingSnapshotLookups = hydrateSnapshotLookups(existingSnapshot || {}); |
66 | | - |
67 | | - // Build a set of scratch collection UIDs to exclude |
68 | | - const scratchCollectionUids = new Set( |
69 | | - (workspaces.workspaces || []) |
70 | | - .map((w) => w.scratchCollectionUid) |
71 | | - .filter(Boolean) |
72 | | - ); |
73 | | - |
74 | | - const activeWorkspace = workspaces.workspaces.find( |
75 | | - (w) => w.uid === workspaces.activeWorkspaceUid |
76 | | - ); |
77 | | - |
78 | | - const activeWorkspaceCollectionPaths = new Set( |
79 | | - (activeWorkspace?.collections || []) |
80 | | - .map((collection) => collection?.path) |
81 | | - .filter(Boolean) |
82 | | - .map((collectionPath) => normalizePath(collectionPath)) |
83 | | - ); |
84 | | - |
85 | | - const existingDevTools = existingSnapshot?.extras?.devTools ?? {}; |
86 | | - |
87 | | - const snapshot = { |
88 | | - activeWorkspacePath: activeWorkspace?.pathname || null, |
89 | | - extras: { |
90 | | - devTools: { |
91 | | - open: logs.isConsoleOpen, |
92 | | - activeTab: logs.activeTab ?? existingDevTools.activeTab ?? 'terminal', |
93 | | - tabs: Object.assign(existingDevTools.tabs, { |
94 | | - [logs.activeTab]: {} |
95 | | - }) |
96 | | - } |
97 | | - }, |
98 | | - workspaces: [], |
99 | | - collections: [] |
100 | | - }; |
101 | | - |
102 | | - // Track which workspace+collection entries we've serialized from Redux |
103 | | - const serializedCollectionKeys = new Set(); |
104 | | - |
105 | | - (workspaces.workspaces || []).forEach((workspace) => { |
106 | | - if (!workspace.pathname) return; |
107 | | - |
108 | | - const workspaceCollectionPaths = (workspace.collections || []).map((c) => c.path).filter(Boolean); |
109 | | - const normalizedWorkspacePaths = workspaceCollectionPaths.map((p) => normalizePath(p)); |
110 | | - const isActiveWorkspace = workspace.uid === workspaces.activeWorkspaceUid; |
111 | | - const existingWorkspace = existingSnapshotLookups.workspacesByPath[normalizePath(workspace.pathname)]; |
112 | | - |
113 | | - // Resolve lastActiveCollectionPathname |
114 | | - let lastActiveCollectionPathname = null; |
115 | | - |
116 | | - if (isActiveWorkspace) { |
117 | | - const activeTab = tabs.tabs.find((t) => t.uid === tabs.activeTabUid); |
118 | | - const activeCollection = activeTab |
119 | | - ? (collections.collections || []).find((c) => c.uid === activeTab.collectionUid) |
120 | | - : null; |
121 | | - const normalizedPathname = activeCollection?.pathname ? normalizePath(activeCollection.pathname) : null; |
122 | | - |
123 | | - lastActiveCollectionPathname = normalizedPathname && normalizedWorkspacePaths.includes(normalizedPathname) |
124 | | - ? normalizedPathname |
125 | | - : null; |
126 | | - } else { |
127 | | - // For non-active workspaces, preserve from existing snapshot |
128 | | - lastActiveCollectionPathname = existingWorkspace?.lastActiveCollectionPathname || null; |
129 | | - } |
130 | | - |
131 | | - const workspaceSorting = isActiveWorkspace |
132 | | - ? activeWorkspaceCollectionSortOrder |
133 | | - : normalizeWorkspaceSorting(existingWorkspace?.sorting); |
134 | | - |
135 | | - snapshot.workspaces.push({ |
136 | | - pathname: workspace.pathname, |
137 | | - environment: '', |
138 | | - lastActiveCollectionPathname, |
139 | | - sorting: workspaceSorting, |
140 | | - collections: [...workspaceCollectionPaths] |
141 | | - }); |
142 | | - }); |
143 | | - |
144 | | - (collections.collections || []).forEach((collection) => { |
145 | | - // Skip scratch collections and collections without pathname |
146 | | - if (!collection.pathname || scratchCollectionUids.has(collection.uid)) { |
147 | | - return; |
148 | | - } |
149 | | - |
150 | | - const normalizedPath = normalizePath(collection.pathname); |
151 | | - |
152 | | - // Persist tab state only for the active workspace's collections. |
153 | | - // For non-active workspaces, preserve the last persisted snapshot entries. |
154 | | - if (activeWorkspace && !activeWorkspaceCollectionPaths.has(normalizedPath)) { |
155 | | - return; |
156 | | - } |
157 | | - |
158 | | - const workspacePathname = activeWorkspace?.pathname || ''; |
159 | | - const collectionSnapshotKey = getWorkspaceCollectionSnapshotKey(workspacePathname, collection.pathname); |
160 | | - const existingCollection = (collectionSnapshotKey && existingSnapshotLookups.collectionsByWorkspaceAndPath?.[collectionSnapshotKey]) |
161 | | - || existingSnapshotLookups.collectionsByPath?.[normalizedPath] |
162 | | - || null; |
163 | | - if (collectionSnapshotKey) { |
164 | | - serializedCollectionKeys.add(collectionSnapshotKey); |
165 | | - } |
166 | | - |
167 | | - // Get transient directory for this collection to filter transient tabs |
168 | | - const transientDirectory = collections.tempDirectories?.[collection.uid]; |
169 | | - |
170 | | - // Filter and serialize tabs, excluding transient requests |
171 | | - const collectionTabs = (tabs.tabs || []) |
172 | | - .filter((t) => t.collectionUid === collection.uid && !shouldExcludeTab(t, transientDirectory)) |
173 | | - .map((t) => serializeTab(t, collection)); |
174 | | - |
175 | | - const activeTabInCollection = (tabs.tabs || []).find( |
176 | | - (t) => t.collectionUid === collection.uid && t.uid === tabs.activeTabUid && !shouldExcludeTab(t, transientDirectory) |
177 | | - ); |
178 | | - |
179 | | - const selectedEnvironment = (collection.environments || []).find((env) => env.uid === collection.activeEnvironmentUid); |
180 | | - const environmentPathFromRedux = getCollectionEnvironmentPath(collection, selectedEnvironment, ''); |
181 | | - const selectedEnvironmentFromRedux = selectedEnvironment?.name || ''; |
182 | | - const existingEnvironmentPath = existingCollection?.environment?.collection || existingCollection?.environmentPath || ''; |
183 | | - const existingSelectedEnvironment = existingCollection?.selectedEnvironment || ''; |
184 | | - const shouldPreserveExistingEnvironment = collection.mountStatus !== 'mounted' |
185 | | - && !environmentPathFromRedux |
186 | | - && !selectedEnvironmentFromRedux; |
187 | | - const environmentPath = shouldPreserveExistingEnvironment ? existingEnvironmentPath : environmentPathFromRedux; |
188 | | - const selectedEnvironmentName = shouldPreserveExistingEnvironment |
189 | | - ? existingSelectedEnvironment |
190 | | - : selectedEnvironmentFromRedux; |
191 | | - |
192 | | - snapshot.collections.push({ |
193 | | - pathname: collection.pathname, |
194 | | - workspacePathname, |
195 | | - environment: { |
196 | | - collection: environmentPath, |
197 | | - global: globalEnvironments.activeGlobalEnvironmentUid || '' |
198 | | - }, |
199 | | - environmentPath, |
200 | | - selectedEnvironment: selectedEnvironmentName, |
201 | | - isOpen: !collection.collapsed, |
202 | | - isMounted: collection.mountStatus === 'mounted', |
203 | | - activeTab: serializeActiveTab(activeTabInCollection, collection), |
204 | | - tabs: collectionTabs |
205 | | - }); |
206 | | - }); |
207 | | - |
208 | | - const pendingHydrationPaths = new Set( |
209 | | - (snapshotHydration?.pendingCollectionPathnames || []).map((pathname) => normalizePath(pathname)) |
210 | | - ); |
211 | | - |
212 | | - // Preserve collections from existing snapshot that aren't currently loaded in Redux |
213 | | - // and collections that are still pending hydration during workspace switch. |
214 | | - const existingCollections = Object.values(existingSnapshotLookups.collectionsByWorkspaceAndPath || {}); |
215 | | - const fallbackCollections = Object.values(existingSnapshotLookups.collectionsByPath || {}); |
216 | | - |
217 | | - (existingCollections.length > 0 ? existingCollections : fallbackCollections).forEach((existingCollection) => { |
218 | | - const normalizedPath = normalizePath(existingCollection.pathname || ''); |
219 | | - const workspacePathname = existingCollection.workspacePathname || ''; |
220 | | - const collectionSnapshotKey = getWorkspaceCollectionSnapshotKey(workspacePathname, existingCollection.pathname); |
221 | | - const isSerializedCollection = collectionSnapshotKey && serializedCollectionKeys.has(collectionSnapshotKey); |
222 | | - const shouldPreservePendingHydration = pendingHydrationPaths.has(normalizedPath) |
223 | | - && activeWorkspace?.pathname |
224 | | - && normalizePath(workspacePathname) === normalizePath(activeWorkspace.pathname); |
225 | | - |
226 | | - if (!normalizedPath || (isSerializedCollection && !shouldPreservePendingHydration)) { |
227 | | - return; |
228 | | - } |
229 | | - |
230 | | - const existingTabs = (collectionSnapshotKey && existingSnapshotLookups.tabsByWorkspaceAndCollectionPath?.[collectionSnapshotKey]) |
231 | | - || existingSnapshotLookups.tabsByCollectionPath?.[normalizedPath]; |
232 | | - |
233 | | - snapshot.collections.push({ |
234 | | - pathname: existingCollection.pathname, |
235 | | - workspacePathname, |
236 | | - environment: { |
237 | | - collection: existingCollection.environment?.collection || existingCollection.environmentPath || '', |
238 | | - global: existingCollection.environment?.global || '' |
239 | | - }, |
240 | | - environmentPath: existingCollection.environment?.collection || existingCollection.environmentPath || '', |
241 | | - selectedEnvironment: existingCollection.selectedEnvironment || '', |
242 | | - isOpen: typeof existingCollection.isOpen === 'boolean' ? existingCollection.isOpen : false, |
243 | | - isMounted: typeof existingCollection.isMounted === 'boolean' ? existingCollection.isMounted : false, |
244 | | - activeTab: existingTabs?.activeTab || existingCollection.activeTab || null, |
245 | | - tabs: Array.isArray(existingTabs?.tabs) |
246 | | - ? existingTabs.tabs |
247 | | - : (Array.isArray(existingCollection.tabs) ? existingCollection.tabs : []) |
248 | | - }); |
249 | | - }); |
250 | | - |
251 | | - return snapshot; |
252 | | -}; |
253 | | - |
254 | | -/** |
255 | | - * Schedule a debounced save of the snapshot |
256 | | - */ |
257 | 16 | const scheduleSave = (getState) => { |
258 | 17 | if (saveTimer) { |
259 | 18 | clearTimeout(saveTimer); |
@@ -305,7 +64,6 @@ export const snapshotMiddleware = ({ getState }) => (next) => (action) => { |
305 | 64 | return result; |
306 | 65 | } |
307 | 66 |
|
308 | | - // Only save if snapshot is ready (app has finished initial loading) |
309 | 67 | const state = getState(); |
310 | 68 | if (state.app.snapshotReady && SAVE_TRIGGERS.has(action.type)) { |
311 | 69 | scheduleSave(getState); |
|
0 commit comments