Skip to content

Commit c0581e7

Browse files
committed
feat: add hybrid IndexedDB workspace persistence with migration
1 parent ffa2fe0 commit c0581e7

6 files changed

Lines changed: 580 additions & 33 deletions

File tree

DEV-GUIDE.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,41 @@ The file open size limit is configured in [src/toolbarActions/toolbarFunctions.j
88
- `MAX_FILE_SIZE` is derived from it as bytes.
99

1010
To change the limit, update `MAX_FILE_SIZE_MB` only.
11+
12+
## Workspace persistence (hybrid storage)
13+
14+
Workspace persistence now uses a hybrid model:
15+
16+
- Files/binary artifacts (imports/exports) stay on the browser filesystem path.
17+
- Graph/session metadata is stored in IndexedDB (`concore-editor-storage`) when available.
18+
- Local storage is used as a fallback if IndexedDB is unavailable.
19+
20+
### IndexedDB schema (v1)
21+
22+
- `graphs` store (key: `id`): graph snapshot payloads used by autosave/recovery.
23+
- `meta` store (key: `key`):
24+
- `graph_order`
25+
- `session`
26+
- `author_name`
27+
- `migrated_local_storage_v1`
28+
29+
### Migration behavior
30+
31+
On first successful IndexedDB init, legacy localStorage data is migrated once:
32+
33+
- all graph snapshots from `ALL_GRAPHS`
34+
- session metadata from `SESSION_STATE` (if present)
35+
- author metadata from `AUTHOR_NAME`
36+
37+
### Fallback behavior
38+
39+
If IndexedDB cannot be initialized, the app stays functional with localStorage fallback.
40+
The same public storage manager API is used in both cases.
41+
42+
### Manual verification checklist
43+
44+
1. Open multiple graphs, switch active tab, reload page.
45+
2. Confirm open tabs and active tab are restored.
46+
3. Edit graphs and verify autosave keeps action history.
47+
4. Corrupt one graph record manually and confirm app still loads remaining workspace.
48+
5. Simulate IndexedDB unavailability and verify fallback load/save still works.

src/GraphWorkspace.jsx

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,65 @@ const GraphComp = (props) => {
1313
const { dispatcher, superState } = props;
1414

1515
React.useEffect(() => {
16-
const allIDs = localStorageManager.getAllGraphs();
17-
const validIDs = allIDs.filter((id) => typeof id === 'string' && id && localStorageManager.get(id) !== null);
18-
if (validIDs.length !== allIDs.length) {
19-
allIDs.filter((id) => !validIDs.includes(id)).forEach((id) => localStorageManager.remove(id));
20-
}
21-
if (validIDs.length === 0) return;
22-
dispatcher({ type: T.ADD_GRAPH_BULK, payload: validIDs.map((graphID) => ({ graphID })) });
16+
let mounted = true;
17+
const restoreWorkspace = async () => {
18+
await localStorageManager.initialize();
19+
if (!mounted) return;
20+
21+
const session = localStorageManager.getSession();
22+
const allIDs = Array.isArray(session?.openGraphIDs) && session.openGraphIDs.length
23+
? session.openGraphIDs
24+
: localStorageManager.getAllGraphs();
25+
const validIDs = allIDs.filter(
26+
(id) => typeof id === 'string' && id && localStorageManager.get(id) !== null,
27+
);
28+
if (validIDs.length !== allIDs.length) {
29+
allIDs.filter((id) => !validIDs.includes(id)).forEach((id) => localStorageManager.remove(id));
30+
}
31+
32+
if (Array.isArray(session?.fileState)) {
33+
dispatcher({ type: T.SET_FILE_STATE, payload: session.fileState });
34+
}
35+
if (typeof session?.uploadedDirName === 'string') {
36+
dispatcher({ type: T.SET_DIR_NAME, payload: session.uploadedDirName });
37+
}
38+
39+
if (validIDs.length === 0) return;
40+
dispatcher({
41+
type: T.ADD_GRAPH_BULK,
42+
payload: {
43+
graphs: validIDs.map((graphID) => ({ graphID })),
44+
activeGraphID: session?.activeGraphID || null,
45+
},
46+
});
47+
};
48+
restoreWorkspace();
49+
50+
return () => {
51+
mounted = false;
52+
};
2353
}, []);
2454

55+
React.useEffect(() => {
56+
const openGraphIDs = superState.graphs
57+
.map((graph) => graph.graphID)
58+
.filter((graphID) => typeof graphID === 'string' && graphID);
59+
const activeGraphID = superState.curGraphIndex >= 0 && superState.curGraphIndex < superState.graphs.length
60+
? superState.graphs[superState.curGraphIndex].graphID
61+
: null;
62+
localStorageManager.saveSession({
63+
openGraphIDs,
64+
activeGraphID,
65+
fileState: superState.fileState,
66+
uploadedDirName: superState.uploadedDirName,
67+
});
68+
}, [
69+
superState.graphs,
70+
superState.curGraphIndex,
71+
superState.fileState,
72+
superState.uploadedDirName,
73+
]);
74+
2575
// Remote server implementation - Not being used.
2676
// useEffect(() => {
2777
// if (!loadedFromStorage) return;

src/component/fileBrowser.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ const LocalFileBrowser = ({ superState, dispatcher }) => {
5353
}, [superState.fileState]);
5454

5555
const handleSelectFile = (data) => {
56+
if (!data.fileObj) {
57+
toast.info('File handle is not available after reload. Re-open the file/directory.');
58+
return;
59+
}
5660
const fileExtensions = ['exe'];
5761
const fileExt = data.fileObj.name.split('.').pop().toLowerCase();
5862
if (fileExtensions.includes(fileExt)) {

0 commit comments

Comments
 (0)