store.ts (5311B)
1 import { RADROOTS_IDB_DATABASE, RADROOTS_IDB_STORES } from "./config.js"; 2 3 const log_idb = (label: string, payload?: Record<string, unknown>): void => { 4 console.log(`[idb] ${label}`, payload ?? {}); 5 }; 6 7 const RADROOTS_IDB_STORE_SET = new Set(RADROOTS_IDB_STORES); 8 const IDB_BOOTSTRAP_PROMISES = new Map<string, Promise<void>>(); 9 10 const idb_missing_stores = (db: IDBDatabase, stores: string[]): string[] => 11 stores.filter((store) => !db.objectStoreNames.contains(store)); 12 13 const idb_database_exists = async (database: string): Promise<boolean> => { 14 if (typeof indexedDB === "undefined") return false; 15 const list_fn = indexedDB.databases; 16 if (typeof list_fn !== "function") return true; 17 try { 18 const entries = await list_fn.call(indexedDB); 19 return entries.some((entry) => entry.name === database); 20 } catch { 21 return true; 22 } 23 }; 24 25 const idb_open = (database: string, version?: number, stores?: string[]): Promise<IDBDatabase> => 26 new Promise((resolve, reject) => { 27 const request = indexedDB.open(database, version); 28 request.onblocked = () => { 29 log_idb(`open_blocked`, { database, version, stores }); 30 }; 31 request.onupgradeneeded = () => { 32 if (!stores || stores.length === 0) return; 33 const db = request.result; 34 const existing_stores = Array.from(db.objectStoreNames); 35 log_idb(`open_upgrade`, { 36 database, 37 version, 38 stores, 39 existing_stores 40 }); 41 for (const store of stores) { 42 if (!db.objectStoreNames.contains(store)) db.createObjectStore(store); 43 } 44 }; 45 request.onsuccess = () => resolve(request.result); 46 request.onerror = () => { 47 if (request.error) reject(request.error); 48 else reject(new Error("idb_open_failed")); 49 }; 50 }); 51 52 const idb_store_ensure_all = async (database: string, stores: string[]): Promise<void> => { 53 if (stores.length === 0) return; 54 const target_stores = Array.from(new Set(stores)); 55 log_idb(`ensure_start`, { database, target_stores }); 56 let attempt = 0; 57 while (attempt < 5) { 58 attempt++; 59 const db = await idb_open(database); 60 const missing = idb_missing_stores(db, target_stores); 61 const version = db.version; 62 const existing_stores = Array.from(db.objectStoreNames); 63 log_idb(`ensure_check`, { 64 database, 65 attempt, 66 version, 67 existing_stores, 68 missing 69 }); 70 if (missing.length === 0) { 71 db.close(); 72 return; 73 } 74 db.close(); 75 try { 76 log_idb(`ensure_upgrade`, { 77 database, 78 attempt, 79 next_version: version + 1, 80 missing 81 }); 82 const upgraded = await idb_open(database, version + 1, missing); 83 const still_missing = idb_missing_stores(upgraded, target_stores); 84 const upgraded_stores = Array.from(upgraded.objectStoreNames); 85 log_idb(`ensure_upgraded`, { 86 database, 87 attempt, 88 version: upgraded.version, 89 upgraded_stores, 90 still_missing 91 }); 92 upgraded.close(); 93 if (still_missing.length === 0) return; 94 } catch (e) { 95 if (e instanceof DOMException && e.name === "VersionError") continue; 96 throw e; 97 } 98 } 99 }; 100 101 const idb_bootstrap_key = (database: string, stores: string[]): string => 102 `${database}:${stores.join("|")}`; 103 104 const idb_store_bootstrap_ready = async (database: string, stores: string[]): Promise<void> => { 105 const key = idb_bootstrap_key(database, stores); 106 const pending = IDB_BOOTSTRAP_PROMISES.get(key); 107 if (pending) return await pending; 108 const promise = idb_store_ensure_all(database, stores); 109 IDB_BOOTSTRAP_PROMISES.set(key, promise); 110 try { 111 await promise; 112 } catch (e) { 113 IDB_BOOTSTRAP_PROMISES.delete(key); 114 throw e; 115 } 116 }; 117 118 export const idb_store_ensure = async (database: string, store: string): Promise<void> => { 119 if (typeof indexedDB === "undefined") return; 120 if (database === RADROOTS_IDB_DATABASE) { 121 await idb_store_bootstrap(database); 122 if (RADROOTS_IDB_STORE_SET.has(store)) return; 123 } 124 await idb_store_ensure_all(database, [store]); 125 }; 126 127 export const idb_store_bootstrap = async (database: string, stores?: string[]): Promise<void> => { 128 if (typeof indexedDB === "undefined") return; 129 const target_stores = stores ?? (database === RADROOTS_IDB_DATABASE ? RADROOTS_IDB_STORES : []); 130 if (target_stores.length === 0) return; 131 await idb_store_bootstrap_ready(database, target_stores); 132 }; 133 134 export const idb_store_exists = async (database: string, store: string): Promise<boolean> => { 135 if (typeof indexedDB === "undefined") return false; 136 const known = await idb_database_exists(database); 137 if (!known) return false; 138 try { 139 const db = await idb_open(database); 140 const exists = db.objectStoreNames.contains(store); 141 db.close(); 142 return exists; 143 } catch { 144 return false; 145 } 146 };