web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

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 };