Skip to content

Commit 6f12fe3

Browse files
Migrate IndexedDB to Dexie.js (#19)
* Checkpoint from VS Code for coding agent session * Migrate IndexedDB to Dexie.js for cleaner API Co-authored-by: Mr-Python-in-China <89737170+Mr-Python-in-China@users.noreply.github.com> * Fix data compatibility - preserve existing IndexedDB structure Co-authored-by: Mr-Python-in-China <89737170+Mr-Python-in-China@users.noreply.github.com>
1 parent c96376a commit 6f12fe3

3 files changed

Lines changed: 54 additions & 138 deletions

File tree

package-lock.json

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

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"axios": "^1.12.2",
4141
"browser-update": "^3.3.63",
4242
"dayjs": "^1.11.18",
43+
"dexie": "^4.2.1",
4344
"lodash": "^4.17.21",
4445
"lodash.debounce": "^4.0.8",
4546
"react": "^19.1.1",
@@ -58,6 +59,7 @@
5859
"devDependencies": {
5960
"@eslint/js": "^9.36.0",
6061
"@types/browser-update": "^3.3.3",
62+
"@types/dexie": "^1.3.32",
6163
"@types/fontkit": "^2.0.8",
6264
"@types/lodash": "^4.17.20",
6365
"@types/lodash.debounce": "^4.0.9",

src/utils/indexedDBUtils.ts

Lines changed: 37 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,36 @@ import type {
55
} from "@/types/contestData";
66
import configSchema from "../../typst-template/config-schema.json";
77
import Ajv from "ajv";
8-
9-
const DB_NAME = "cnoi-statement-generator";
10-
const DB_VERSION = 1;
11-
const CONFIG_STORE = "config";
12-
const IMAGE_STORE = "images";
8+
import Dexie from "dexie";
139

1410
const ajv = new Ajv({ allErrors: true });
1511
const validateSchema = ajv.compile(configSchema);
1612

1713
/**
18-
* Open IndexedDB connection
14+
* Dexie database schema
1915
*/
20-
function openDB(): Promise<IDBDatabase> {
21-
return new Promise((resolve, reject) => {
22-
const request = indexedDB.open(DB_NAME, DB_VERSION);
23-
24-
request.onerror = () => reject(request.error);
25-
request.onsuccess = () => resolve(request.result);
26-
27-
request.onupgradeneeded = (event) => {
28-
const db = (event.target as IDBOpenDBRequest).result;
29-
30-
// Create config store
31-
if (!db.objectStoreNames.contains(CONFIG_STORE)) {
32-
db.createObjectStore(CONFIG_STORE);
33-
}
34-
35-
// Create image store with uuid as key
36-
if (!db.objectStoreNames.contains(IMAGE_STORE)) {
37-
db.createObjectStore(IMAGE_STORE, { keyPath: "uuid" });
38-
}
39-
};
40-
});
16+
class CnoiDatabase extends Dexie {
17+
// Using Table instead of EntityTable to support non-inlined keys
18+
config!: Dexie.Table<StoredContestData, string>;
19+
images!: Dexie.Table<EditorImageData, string>;
20+
21+
constructor() {
22+
super("cnoi-statement-generator");
23+
this.version(1).stores({
24+
config: "", // Empty string means the key is not part of the object (out-of-line key)
25+
images: "uuid",
26+
});
27+
}
4128
}
4229

30+
const db = new CnoiDatabase();
31+
4332
/**
4433
* Save config to IndexedDB
4534
*/
4635
export async function saveConfigToDB(
4736
data: ContestDataWithImages,
4837
): Promise<void> {
49-
const db = await openDB();
50-
const transaction = db.transaction(CONFIG_STORE, "readwrite");
51-
const store = transaction.objectStore(CONFIG_STORE);
52-
5338
// Remove url field from images for storage
5439
const storedData: StoredContestData = {
5540
...data,
@@ -59,18 +44,7 @@ export async function saveConfigToDB(
5944
})),
6045
};
6146

62-
store.put(storedData, "current");
63-
64-
return new Promise((resolve, reject) => {
65-
transaction.oncomplete = () => {
66-
db.close();
67-
resolve();
68-
};
69-
transaction.onerror = () => {
70-
db.close();
71-
reject(transaction.error);
72-
};
73-
});
47+
await db.config.put(storedData, "current");
7448
}
7549

7650
/**
@@ -80,124 +54,49 @@ export async function loadConfigFromDB(): Promise<{
8054
data: StoredContestData;
8155
images: Map<string, Blob>; // uuid -> Blob
8256
} | null> {
83-
const db = await openDB();
84-
const transaction = db.transaction([CONFIG_STORE, IMAGE_STORE], "readonly");
85-
const configStore = transaction.objectStore(CONFIG_STORE);
86-
const imageStore = transaction.objectStore(IMAGE_STORE);
87-
88-
return new Promise((resolve, reject) => {
89-
const configRequest = configStore.get("current");
90-
91-
configRequest.onsuccess = async () => {
92-
const storedData = configRequest.result as StoredContestData | undefined;
93-
94-
if (!storedData) {
95-
db.close();
96-
resolve(null);
97-
return;
98-
}
57+
const storedData = await db.config.get("current");
9958

100-
// Load all images
101-
const imageMap = new Map<string, Blob>();
102-
const imagePromises = (storedData.images || []).map(
103-
(img) =>
104-
new Promise<void>((resolveImg, rejectImg) => {
105-
const imgRequest = imageStore.get(img.uuid);
106-
imgRequest.onsuccess = () => {
107-
const imageData = imgRequest.result as
108-
| EditorImageData
109-
| undefined;
110-
if (imageData) {
111-
imageMap.set(imageData.uuid, imageData.blob);
112-
}
113-
resolveImg();
114-
};
115-
imgRequest.onerror = () => rejectImg(imgRequest.error);
116-
}),
117-
);
59+
if (!storedData) {
60+
return null;
61+
}
11862

119-
try {
120-
await Promise.all(imagePromises);
121-
db.close();
63+
// Load all images
64+
const imageMap = new Map<string, Blob>();
65+
const imageUuids = (storedData.images || []).map((img) => img.uuid);
12266

123-
resolve({ data: storedData, images: imageMap });
124-
} catch (error) {
125-
db.close();
126-
reject(error);
67+
if (imageUuids.length > 0) {
68+
const images = await db.images.bulkGet(imageUuids);
69+
images.forEach((imageData) => {
70+
if (imageData) {
71+
imageMap.set(imageData.uuid, imageData.blob);
12772
}
128-
};
73+
});
74+
}
12975

130-
configRequest.onerror = () => {
131-
db.close();
132-
reject(configRequest.error);
133-
};
134-
});
76+
return { data: storedData, images: imageMap };
13577
}
13678

13779
/**
13880
* Save image to IndexedDB
13981
*/
14082
export async function saveImageToDB(uuid: string, blob: Blob): Promise<void> {
141-
const db = await openDB();
142-
const transaction = db.transaction(IMAGE_STORE, "readwrite");
143-
const store = transaction.objectStore(IMAGE_STORE);
144-
145-
const imageData: EditorImageData = { uuid, blob };
146-
store.put(imageData);
147-
148-
return new Promise((resolve, reject) => {
149-
transaction.oncomplete = () => {
150-
db.close();
151-
resolve();
152-
};
153-
transaction.onerror = () => {
154-
db.close();
155-
reject(transaction.error);
156-
};
157-
});
83+
await db.images.put({ uuid, blob });
15884
}
15985

16086
/**
16187
* Delete image from IndexedDB
16288
*/
16389
export async function deleteImageFromDB(uuid: string): Promise<void> {
164-
const db = await openDB();
165-
const transaction = db.transaction(IMAGE_STORE, "readwrite");
166-
const store = transaction.objectStore(IMAGE_STORE);
167-
168-
store.delete(uuid);
169-
170-
return new Promise((resolve, reject) => {
171-
transaction.oncomplete = () => {
172-
db.close();
173-
resolve();
174-
};
175-
transaction.onerror = () => {
176-
db.close();
177-
reject(transaction.error);
178-
};
179-
});
90+
await db.images.delete(uuid);
18091
}
18192

18293
/**
18394
* Clear all data from IndexedDB
18495
*/
18596
export async function clearDB(): Promise<void> {
186-
const db = await openDB();
187-
const transaction = db.transaction([CONFIG_STORE, IMAGE_STORE], "readwrite");
188-
189-
transaction.objectStore(CONFIG_STORE).clear();
190-
transaction.objectStore(IMAGE_STORE).clear();
191-
192-
return new Promise((resolve, reject) => {
193-
transaction.oncomplete = () => {
194-
db.close();
195-
resolve();
196-
};
197-
transaction.onerror = () => {
198-
db.close();
199-
reject(transaction.error);
200-
};
97+
await db.transaction("rw", [db.config, db.images], async () => {
98+
await db.config.clear();
99+
await db.images.clear();
201100
});
202101
}
203102

0 commit comments

Comments
 (0)