idb-managed
Version:
Easy APIs for IndexedDB, with DB manager to manage local DBs. Based on idb.
459 lines (439 loc) • 14.4 kB
text/typescript
/**
* @file Wrap idb APIs for idb-managed
*/
import { deduplicateList } from './lib/utils';
import {
IndexRange,
ItemConfig,
TableConfig,
IndexOfTable,
TableIndexRange
} from './interface';
const IDB = require('./lib/idb');
const IDB_MANAGER_VERSION = 1;
const IDB_MANAGER_DB_NAME = 'IDB_MANAGER_DB';
const IDB_MANAGER_DB_TABLE_NAME = 'IDB_MANAGER_STORE';
const IDB_MANAGER_DB_TABLE_INDEX_NAME = 'dbName';
const UPDATETIME_KEYNAME = 'updateTime';
const EXPIRETIME_KEYNAME = 'expireTime';
interface ItemInDBManager {
dbName: string;
tableList: TableConfig[];
version: number;
}
interface ItemInTable {
[key: string]: any;
expireTime: number;
updateTime: number;
}
interface DB {
name: string;
tableList: TableConfig[];
version: number;
}
function indexRange2DBKey (indexRange: IndexRange) {
const {
onlyIndex,
lowerIndex,
upperIndex,
lowerExclusive = false,
upperExclusive = false
} = indexRange;
if (onlyIndex !== undefined) {
return IDBKeyRange.only(onlyIndex);
} else if (lowerIndex !== undefined && upperIndex !== undefined) {
return IDBKeyRange.bound(
lowerIndex,
upperIndex,
lowerExclusive,
upperExclusive
);
} else if (lowerIndex !== undefined) {
return IDBKeyRange.lowerBound(lowerIndex, lowerExclusive);
} else {
return IDBKeyRange.upperBound(upperIndex, upperExclusive);
}
}
function itemWrapper (itemConfig: ItemConfig): ItemInTable {
const currentTime = Date.now();
return {
...itemConfig.item,
[UPDATETIME_KEYNAME]: currentTime,
[EXPIRETIME_KEYNAME]:
itemConfig.itemDuration !== undefined
? itemConfig.itemDuration + currentTime
: -1
};
}
function itemUnwrapper (item: ItemInTable) {
if (!item) {
return null;
} else if (item.expireTime > 0 && item.expireTime < Date.now()) {
return null;
} else {
delete item.updateTime;
delete item.expireTime;
return item;
}
}
async function registerDBInManager (dbInfo: DB) {
const dbManager = await openDBManager();
const dbAlreadyInManager = ((await getItemFromDB(
dbManager as any,
IDB_MANAGER_DB_TABLE_NAME,
dbInfo.name
)) as any) as ItemInDBManager;
if (!dbAlreadyInManager || dbInfo.version > dbAlreadyInManager.version) {
// Update db in manager
const addDBTrans = dbManager.transaction(
IDB_MANAGER_DB_TABLE_NAME,
'readwrite'
);
const table = addDBTrans.objectStore(IDB_MANAGER_DB_TABLE_NAME);
const dbItem: ItemInDBManager = {
dbName: dbInfo.name,
tableList: dbInfo.tableList,
version: dbInfo.version
};
table.put(
itemWrapper({
item: dbItem,
tableName: IDB_MANAGER_DB_TABLE_NAME
})
);
await addDBTrans.complete;
dbManager.close();
}
}
async function unregisterDBInManager (dbName: string) {
const dbManager = await openDBManager();
const deleteTrans = dbManager.transaction(
IDB_MANAGER_DB_TABLE_NAME,
'readwrite'
);
const table = deleteTrans.objectStore(IDB_MANAGER_DB_TABLE_NAME);
table.delete(dbName);
await deleteTrans.complete;
dbManager.close();
}
async function createDB (dbInfo: DB) {
await registerDBInManager(dbInfo);
const db = await IDB.open(
dbInfo.name,
dbInfo.version as number,
(upgradeDB: any) => {
upgradeDBWithTableList(upgradeDB as any, dbInfo.tableList);
}
);
return db;
}
async function openDBManager () {
return await IDB.open(
IDB_MANAGER_DB_NAME,
IDB_MANAGER_VERSION,
// In case DB Manager has not been created.
(upgradeDB: any) => {
upgradeDBManager(upgradeDB as any);
}
);
}
async function openDB (dbName: string) {
const dbManager = await openDBManager();
const dbAlreadyInManager = ((await getItemFromDB(
dbManager as any,
IDB_MANAGER_DB_TABLE_NAME,
dbName
)) as any) as ItemInDBManager | null;
dbManager.close();
if (dbAlreadyInManager) {
const db = await IDB.open(
dbAlreadyInManager.dbName,
dbAlreadyInManager.version as number,
// In case this DB has not been created.
(upgradeDB: any) => {
upgradeDBWithTableList(
upgradeDB as any,
dbAlreadyInManager.tableList || []
);
}
);
return db;
} else {
return null;
}
}
async function getItemFromDB (
db: IDBDatabase,
tableName: string,
primaryKeyValue: any
) {
if (db.objectStoreNames.contains(tableName)) {
const trans: any = db.transaction(tableName, 'readonly');
try {
const table = trans.objectStore(tableName);
const itemInTable = ((await table.get(
primaryKeyValue
)) as any) as ItemInTable;
return itemUnwrapper(itemInTable) as any;
} finally {
try {
await trans.complete;
} catch (e) {
}
}
} else {
return null;
}
}
function upgradeDBManager (upgradeDB: IDBDatabase) {
upgradeDB.createObjectStore(IDB_MANAGER_DB_TABLE_NAME, {
keyPath: IDB_MANAGER_DB_TABLE_INDEX_NAME
});
}
function upgradeDBWithTableList (
upgradeDB: IDBDatabase,
tableList: TableConfig[]
) {
try {
tableList.forEach(tableConfig => {
// If table already exists.
if (
upgradeDB.objectStoreNames.contains(
tableConfig.tableName as string
)
) {
const currentTable = upgradeDB
.transaction(tableConfig.tableName as string)
.objectStore(tableConfig.tableName as string);
// Create new index for present table.
(tableConfig.indexList || []).forEach(
(theIndex: IndexOfTable) => {
if (
!currentTable.indexNames.contains(
theIndex.indexName
)
) {
currentTable.createIndex(
theIndex.indexName,
theIndex.indexName,
{ unique: theIndex.unique }
);
}
}
);
// Else create new table.
} else {
const tablePrimaryKey = tableConfig.primaryKey || 'id';
const tableToCreate = upgradeDB.createObjectStore(
tableConfig.tableName as string,
{
keyPath: tablePrimaryKey,
...(tablePrimaryKey === 'id'
? { autoIncrement: true }
: {})
}
);
// Set index of primaryKey.
tableToCreate.createIndex(tablePrimaryKey, tablePrimaryKey, {
unique: true
});
// Set indexes defined in tableConfig
(tableConfig.indexList || []).forEach(
(theIndex: IndexOfTable) => {
tableToCreate.createIndex(
theIndex.indexName,
theIndex.indexName,
{ unique: theIndex.unique }
);
}
);
// Set index of updateTime for data ordering priority.
tableToCreate.createIndex(
UPDATETIME_KEYNAME,
UPDATETIME_KEYNAME,
{ unique: false }
);
// Set index of expireTime for expired data deletion.
tableToCreate.createIndex(
EXPIRETIME_KEYNAME,
EXPIRETIME_KEYNAME,
{ unique: false }
);
}
});
} catch (e) {
upgradeDB.close(); // Close upgraded DB to trigger the failure of this opening process.
}
}
async function atomicTrans (transaction: any, db: any, tryStatement: Function) {
try {
await tryStatement();
await transaction.complete;
} catch (transError) {
try {
// To roll back all operations in this transaction if any error happens.
transaction.abort();
} catch (e) {
// Do nothing if transaction abort failed.
}
try {
// To catch the Promise error caused by transaction abortion. Otherwise, uncaught rejection will be thrown up.
await transaction.complete;
} catch (e) {
}
// Throw specific error happened in tryStatement.
throw transError;
} finally {
db.close();
}
}
async function deleteItemsFromDB (db: any, tableIndexRanges: TableIndexRange[]) {
const validRanges = tableIndexRanges.filter(indexRange => {
return db.objectStoreNames.contains(indexRange.tableName);
});
const dedupTableNameList: string[] = deduplicateList(validRanges.map(tableIndexRange => tableIndexRange.tableName));
const deleteItemsTrans = db.transaction(dedupTableNameList, 'readwrite');
await atomicTrans(deleteItemsTrans, db, async () => {
await Promise.all(validRanges.map(tableIndexRange => {
const { tableName, indexRange } = tableIndexRange;
const table = deleteItemsTrans.objectStore(tableName);
if (!indexRange) {
return table.clear();
} else {
return new Promise(function (resolve) {
table.index(indexRange.indexName).iterateCursor(indexRange2DBKey(indexRange), (cursor: any) => {
if (!cursor) {
resolve();
return;
}
table.delete(cursor.primaryKey);
cursor.continue();
});
});
}
}));
});
}
export async function addItems (dbInfo: DB, items: ItemConfig[]) {
const dedupTableNameList: string[] = deduplicateList(
items.map(item => item.tableName)
);
await deleteItems(
dbInfo.name,
dedupTableNameList.map(tableName => {
return {
tableName: tableName,
indexRange: {
indexName: EXPIRETIME_KEYNAME,
upperIndex: +new Date(),
upperExclusive: false
}
};
})
);
const db = await createDB(dbInfo);
const addItemsTrans = db.transaction(dedupTableNameList, 'readwrite');
await atomicTrans(addItemsTrans, db, async () => {
await Promise.all(items.map(item => addItemsTrans.objectStore(item.tableName).put(itemWrapper(item))
));
});
}
export async function getItem (
dbName: string,
tableName: string,
primaryKeyValue: any
) {
const db = await openDB(dbName);
if (db) {
try {
const item = await getItemFromDB(
(db as any) as IDBDatabase,
tableName,
primaryKeyValue
);
return item;
} catch (e) {
throw e;
} finally {
db.close();
}
} else {
return null;
}
}
export async function getItemsInRange (
dbName: string,
tableIndexRange: TableIndexRange
) {
const { tableName, indexRange } = tableIndexRange;
const db = await openDB(dbName);
if (db) {
try {
let items: any[] = [];
if (!db.objectStoreNames.contains(tableName)) {
// Do nothing if table does not exist.
} else {
const trans = db.transaction(tableName, 'readonly');
try {
const table = trans.objectStore(tableName);
if (!indexRange) {
// Get all items in table if indexRange is undefined
let wrappedItems = await table.getAll();
items = (wrappedItems || [])
.map(itemUnwrapper)
.filter((item: any) => {
return item !== null;
});
} else {
await new Promise(function (resolve) {
table.index(indexRange.indexName).iterateCursor(indexRange2DBKey(indexRange), (cursor: any) => {
if (!cursor) {
resolve();
return;
}
var item = itemUnwrapper(cursor.value);
item && items.push(item);
cursor.continue();
});
});
}
} finally {
try {
await trans.complete;
} catch (e) {
}
}
}
return items;
} catch (e) {
throw e;
} finally {
db.close();
}
} else {
return [];
}
}
export async function deleteDB (dbName: string) {
await unregisterDBInManager(dbName);
await IDB.delete(dbName);
}
export async function deleteItems (
dbName: string,
tableIndexRanges: TableIndexRange[]
) {
const db = await openDB(dbName);
if (db) {
await deleteItemsFromDB(db, tableIndexRanges);
} else {
// If db does not exist, no need to deleteItems at all.
return;
}
}
export default {
addItems,
getItem,
getItemsInRange,
deleteDB,
deleteItems
};