UNPKG

mosquito-transport-js

Version:

Javascript web sdk for mosquito-transport (https://github.com/brainbehindx/mosquito-transport)

273 lines (247 loc) 10.7 kB
import { deserialize, serialize } from "entity-serializer"; import { Scoped } from "./variables"; import { CACHE_STORAGE_PATH } from "./values"; const PRIMARY_KEY = 'primary_key'; const SUB_TABLE = { METADATA: p => `${p}:META`, DATA: p => `${p}:DATA` }; /** * this method linearize read/write for individual access_id on the file system ensuring consistency across concurrent operations * * @param {any} builder * @param {string} access_id * @returns {(task: (system: { set: (table: string, primary_key: string, value: {}) => Promise<void>, delete: (table: string, primary_key: string) => Promise<void>, find: (table: string, primary_key: string, extractions: string[]) => Promise<{}>, list: (table: string, extractions: string[]) => Promise<[string, {}][]> }) => any) => Promise<any>} */ export const useFS = (builder, access_id) => async (task) => { const { projectUrl, dbUrl, dbName } = builder; const nodeId = typeof builder === 'string' ? `${builder}_${access_id}` : `${projectUrl}_${dbUrl}_${dbName}_${access_id}`; const thatProcess = Scoped.linearDbOpenProcess[nodeId]; const thisPromise = new Promise(async (resolve, reject) => { try { if (thatProcess !== undefined) await thatProcess; } catch (_) { } try { resolve(await task(getSystem(builder))); } catch (error) { console.error('useFS err:', error, ' builder:', builder); reject(error); } finally { if (Scoped.linearDbOpenProcess[nodeId] === thisPromise) delete Scoped.linearDbOpenProcess[nodeId]; } }); Scoped.linearDbOpenProcess[nodeId] = thisPromise; return (await thisPromise); }; export const getSystem = (builder) => { const { projectUrl } = builder; const DB_NAME = `${CACHE_STORAGE_PATH}::${purifyFilepath(typeof builder === 'string' ? builder : projectUrl)}`; /** * @param {string} table * @param {undefined | boolean} upgradable * @returns {Promise<{ db: IDBDatabase, end: () => void }>} */ const prepareDb = async (table, upgradable) => { const linear_id = `${DB_NAME}_${table}`; if (!Scoped.chainedDbOperator[linear_id]) Scoped.chainedDbOperator[linear_id] = { ite: 0 }; ++Scoped.chainedDbOperator[linear_id].ite; const { promise: prevDb, endPromise } = Scoped.chainedDbOperator[linear_id]; const thisPromise = (async () => { const onupgraded = upgradable && (s => { s.createObjectStore(SUB_TABLE.METADATA(table), { keyPath: PRIMARY_KEY }); s.createObjectStore(SUB_TABLE.DATA(table), { keyPath: PRIMARY_KEY }); }); let thisDb = prevDb && await prevDb.catch(() => null); if (!thisDb) { thisDb = await getCoreDB(DB_NAME, undefined, s => { onupgraded?.(s.target.result); }).then(r => r.db); } if (!thisDb.objectStoreNames.contains(SUB_TABLE.METADATA(table))) { if (endPromise) await endPromise.catch(() => null); thisDb.close(); if (!onupgraded) throw 'operation failure caused by inability to upgrade database store'; thisDb = await getCoreDB(DB_NAME, thisDb.version + 1, s => { onupgraded?.(s.target.result); }).then(r => r.db); } return thisDb; })(); // linearize db upgrade let endPromiseCallback; Scoped.chainedDbOperator[linear_id].promise = thisPromise; Scoped.chainedDbOperator[linear_id].endPromise = new Promise(resolve => { endPromiseCallback = resolve; }); const cleanup = async () => { const thatProcess = Scoped.chainedDbOperator[linear_id]; if (--thatProcess.ite) return; delete Scoped.chainedDbOperator[linear_id]; (await thatProcess.promise).close(); }; try { const db = await thisPromise; return { db, end: () => { endPromiseCallback(); setTimeout(cleanup, 900) } }; } catch (_) { endPromiseCallback(); cleanup(); } } return { set: async (table, primary_key, value) => { const { db, end } = await prepareDb(table, true); try { const isFull = 'value' in value; const tx = db.transaction([SUB_TABLE.METADATA(table), SUB_TABLE.DATA(table)].slice(0, isFull ? undefined : 1), 'readwrite'); const meta_store = tx.objectStore(SUB_TABLE.METADATA(table)); if (isFull) { const value_store = tx.objectStore(SUB_TABLE.DATA(table)); const { value: main_value, ...meta } = value; await Promise.all([ meta_store.put({ ...meta, [PRIMARY_KEY]: primary_key }), value_store.put({ value: serialize(main_value), [PRIMARY_KEY]: primary_key }) ].map(resolveIDBRequest)); } else { const newMeta = { ...await resolveIDBRequest(meta_store.get(primary_key)), ...value, [PRIMARY_KEY]: primary_key }; await resolveIDBRequest(meta_store.put(newMeta)); } try { tx.commit(); } catch (_) { } } catch (error) { throw error; } finally { end(); } }, delete: async (table, primary_key) => { const { db, end } = await prepareDb(table); try { const tx = db.transaction([SUB_TABLE.METADATA(table), SUB_TABLE.DATA(table)], 'readwrite'); const meta_store = tx.objectStore(SUB_TABLE.METADATA(table)); const value_store = tx.objectStore(SUB_TABLE.DATA(table)); await Promise.all([ meta_store.delete(primary_key), value_store.delete(primary_key) ].map(resolveIDBRequest)); try { tx.commit(); } catch (_) { } return true; } catch (error) { throw error; } finally { end(); } }, find: async (table, primary_key, extractions) => { const { db, end } = await prepareDb(table, true); const doExtraction = (s) => Object.fromEntries( extractions.filter(v => s.hasOwnProperty(v)) .map(v => [v, s[v]]) ); try { const isFull = extractions.includes('value'); const onlyValue = isFull && extractions.length === 1; const tx = db.transaction( [ onlyValue ? undefined : SUB_TABLE.METADATA(table), isFull ? SUB_TABLE.DATA(table) : undefined ].filter(v => v), 'readonly' ); const meta_store = !onlyValue && tx.objectStore(SUB_TABLE.METADATA(table)); if (isFull) { const value_store = tx.objectStore(SUB_TABLE.DATA(table)); const [m, v] = await Promise.all([ onlyValue ? Promise.resolve() : resolveIDBRequest(meta_store.get(primary_key)), resolveIDBRequest(value_store.get(primary_key)) ]); if ((m || onlyValue) && v) return doExtraction({ ...m, value: deserialize(v.value) }); } else { const m = await resolveIDBRequest(meta_store.get(primary_key)); if (m) return doExtraction({ ...m }); } throw `record matching key:${primary_key} not found`; } catch (error) { throw error; } finally { end(); } }, list: async (table, extractions) => { const { db, end } = await prepareDb(table); try { const tx = db.transaction([SUB_TABLE.METADATA(table)], 'readonly'); const meta_store = tx.objectStore(SUB_TABLE.METADATA(table)); const names = await resolveIDBRequest(meta_store.getAllKeys()); const list_data = await Promise.all(names.map(async primary_key => { if (!extractions.length) return [primary_key, {}]; const obj = await getSystem(builder) .find(table, primary_key, extractions) .catch(() => null); if (!obj) return; return [primary_key, obj]; })); return list_data.filter(v => v); } catch (error) { throw error; } finally { end(); } } }; }; export function purifyFilepath(filename) { if (!filename || typeof filename !== 'string') throw `invalid filename:${filename}`; return filename; } export const FS_PATH = { LIMITER_RESULT: (path, dbUrl, dbName) => `${dbUrl}_${dbName}_${purifyFilepath(path)}_LR`, LIMITER_DATA: (path, dbUrl, dbName) => `${dbUrl}_${dbName}_${purifyFilepath(path)}_LD`, DB_COUNT_QUERY: (path, dbUrl, dbName) => `${dbUrl}_${dbName}_${purifyFilepath(path)}_QC`, FETCHERS: 'FETCHERS' }; /** * @param {string} db_name * @param {number} version * @param {IDBOpenDBRequest['onupgradeneeded']} onupgradeneeded * @returns {Promise<{db: IDBDatabase, event: Event}>} */ export const getCoreDB = (db_name, version, onupgradeneeded) => new Promise((resolve, reject) => { try { const result = indexedDB.open(db_name, version); result.onupgradeneeded = onupgradeneeded; result.onsuccess = (e) => resolve({ db: e.target.result, event: e }); result.onerror = reject; } catch (error) { reject(error); } }); /** * @param {IDBRequest} instance * @returns {Promise<any>} */ export const resolveIDBRequest = (instance) => new Promise((resolve, reject) => { instance.onsuccess = (s) => { resolve(s.result); }; instance.onerror = e => { reject(instance.error); } });