export function openSimpleDB(
name: string,
version: number,
onUpgrade: (db: IDBDatabase) => void
) {
return new Promise<SimpleDB>((resolve, reject) => {
const req = indexedDB.open(name, version);
req.onupgradeneeded = () => {
const db = req.result;
console.log("openRequest.onupgradeneeded", db.name, db.version);
onUpgrade(db);
};
req.onsuccess = () => {
const db = req.result;
console.log("openRequest.onsuccess", db.name);
resolve(new SimpleDB(db));
};
req.onerror = () => {
console.error("openRequest.onerror", req.error);
reject(req.error);
};
req.onblocked = (e) => {
console.error("openRequest.onblocked", e);
reject(e);
};
});
}
class SimpleDB {
constructor(private readonly db: IDBDatabase) {}
store(name: string) {
return new SimpleStore(name, this.db);
}
transaction(
stores: SimpleStore | SimpleStore[],
mode: IDBTransactionMode = "readwrite"
) {
if (Array.isArray(stores)) {
return this.db.transaction(
stores.map((s) => s.name),
mode
);
} else {
return this.db.transaction(stores.name, mode);
}
}
delete() {
return new Promise<boolean>((resolve, reject) => {
const req = indexedDB.deleteDatabase(this.db.name);
req.onupgradeneeded = (e) => {
console.log("Database deleted onupgradeneeded...", this.db.name, e);
reject(e);
};
req.onsuccess = (e) => {
console.log("Database deleted successfully...", this.db.name, e);
resolve(true);
};
req.onerror = (e) => {
console.error("Database deleted failed...", this.db.name, e);
reject(e);
};
req.onblocked = (e) => {
console.error("Database deleted onblocked...", this.db.name, e);
reject(e);
};
});
}
close() {
return this.db.close();
}
}
abstract class AbstractSimpleStore<T extends IDBObjectStore | IDBIndex> {
abstract exec<V>(
tx: IDBTransaction | undefined,
cb: SimpleStoreCallback<T, V>
): Promise<V>;
get(key: IDBValidKey, tx?: IDBTransaction) {
return this.exec<any>(tx, (store, resolve, reject) => {
const req = store.get(key);
handlRequest(req, resolve, reject);
});
}
/**
* 匹配符合的数据(数组)
* @param query 键(主键/索引)的值或者范围,如果是 IDBObjectStore 调用则是指定主键的值或者范围,如果是 IDBIndex 调用则是指定索引的值或者范围
* @param count 指定个数
* @param tx 事务
* @returns 符合的数据(数组)
*/
getAll(
query?: IDBValidKey | IDBKeyRange | null,
count?: number,
tx?: IDBTransaction
) {
return this.exec<any[]>(tx, (store, resolve, reject) => {
const req = store.getAll(query, count);
handlRequest(req, resolve, reject);
});
}
/**
* 匹配指定范围的数据(数组)
* @param lower 获取范围的下限
* @param upper 获取范围的上限
* @param lowerOpen 是否不包括下限, 下限开区间
* @param upperOpen 是否不包括上限, 上限开区间
* @param count 指定个数
* @param tx 事务
* @returns 符合的数据(数组)
*/
getAllBound(
lower: any,
upper: any,
lowerOpen: boolean,
upperOpen: boolean,
count?: number,
tx?: IDBTransaction
) {
return this.getAll(
IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen),
count,
tx
);
}
/**
* 匹配指定下限的数据(数组)
* @param lower 获取范围的下限
* @param lowerOpen 是否不包括下限, 下限开区间
* @param count 指定个数
* @param tx 事务
* @returns 符合的数据(数组)
*/
getAllLB(
lower: any,
lowerOpen: boolean,
count?: number,
tx?: IDBTransaction
) {
return this.getAll(IDBKeyRange.lowerBound(lower, lowerOpen), count, tx);
}
/**
* 匹配指定上限的数据(数组)
* @param upper 获取范围的上限
* @param upperOpen 是否不包括上限, 上限开区间
* @param count 指定个数
* @param tx 事务
* @returns 符合的数据(数组)
*/
getAllUB(
upper: any,
upperOpen: boolean,
count?: number,
tx?: IDBTransaction
) {
return this.getAll(IDBKeyRange.upperBound(upper, upperOpen), count, tx);
}
/**
* 获取指定范围的主键(数组)
* @param query 键(主键/索引)的值或者范围,如果是 IDBObjectStore 调用则是指定主键的值或者范围,如果是 IDBIndex 调用则是指定索引的值或者范围
* @param count 指定个数
* @param tx 事务
* @returns 主键(数组)
*/
getAllKeys(
query?: IDBValidKey | IDBKeyRange,
count?: number,
tx?: IDBTransaction
) {
return this.exec<IDBValidKey[]>(tx, (store, resolve, reject) => {
const req = store.getAllKeys(query, count);
handlRequest(req, resolve, reject);
});
}
/**
* 匹配指定范围的数据(数组)
* @param lower 获取范围的下限
* @param upper 获取范围的上限
* @param lowerOpen 是否不包括下限, 下限开区间
* @param upperOpen 是否不包括上限, 上限开区间
* @param count 指定个数
* @param tx 事务
* @returns 符合的数据(数组)
*/
getAllKeysBound(
lower: any,
upper: any,
lowerOpen: boolean,
upperOpen: boolean,
count?: number,
tx?: IDBTransaction
) {
return this.getAllKeys(
IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen),
count,
tx
);
}
/**
* 匹配指定下限的数据(数组)
* @param lower 获取范围的下限
* @param lowerOpen 是否不包括下限, 下限开区间
* @param count 指定个数
* @param tx 事务
* @returns 符合的数据(数组)
*/
getAllKeysLB(
lower: any,
lowerOpen: boolean,
count?: number,
tx?: IDBTransaction
) {
return this.getAllKeys(IDBKeyRange.lowerBound(lower, lowerOpen), count, tx);
}
/**
* 匹配指定上限的数据(数组)
* @param upper 获取范围的上限
* @param upperOpen 是否不包括上限, 上限开区间
* @param count 指定个数
* @param tx 事务
* @returns 符合的数据(数组)
*/
getAllKeysUB(
upper: any,
upperOpen: boolean,
count?: number,
tx?: IDBTransaction
) {
return this.getAllKeys(IDBKeyRange.upperBound(upper, upperOpen), count, tx);
}
/**
* 获取指定范围的主键值
* @param query 键(主键/索引)的值或者范围,如果是 IDBObjectStore 调用则是指定主键的值或者范围,如果是 IDBIndex 调用则是指定索引的值或者范围
* @param tx 事务
* @returns 主键值 如果匹配到多个,只会返回第一个
*/
getKey(query: IDBValidKey | IDBKeyRange, tx?: IDBTransaction) {
return this.exec<IDBValidKey | undefined>(tx, (store, resolve, reject) => {
const req = store.getKey(query);
handlRequest(req, resolve, reject);
});
}
/**
* 匹配指定范围的数据
* @param lower 获取范围的下限
* @param upper 获取范围的上限
* @param lowerOpen 是否不包括下限, 下限开区间
* @param upperOpen 是否不包括上限, 上限开区间
* @param tx 事务
* @returns 符合的数据, 返回一个
*/
getKeyBound(
lower: any,
upper: any,
lowerOpen: boolean,
upperOpen: boolean,
tx?: IDBTransaction
) {
return this.getKey(
IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen),
tx
);
}
/**
* 匹配指定下限的数据
* @param lower 获取范围的下限
* @param lowerOpen 是否不包括下限, 下限开区间
* @param tx 事务
* @returns 符合的数据, 返回一个
*/
getKeyLB(lower: any, lowerOpen: boolean, tx?: IDBTransaction) {
return this.getKey(IDBKeyRange.lowerBound(lower, lowerOpen), tx);
}
/**
* 匹配指定上限的数据
* @param upper 获取范围的上限
* @param upperOpen 是否不包括上限, 上限开区间
* @param tx 事务
* @returns 符合的数据, 返回一个
*/
getKeyUB(upper: any, upperOpen: boolean, tx?: IDBTransaction) {
return this.getKey(IDBKeyRange.upperBound(upper, upperOpen), tx);
}
/**
* 遍历所有数据
* @param cb 针对每一条数据的回调处理函数
* @param key 键的值,如果是 IDBObjectStore 调用则是主键的值,如果是 IDBIndex 调用则是索引的值
* @param value 获取的数据
* @param update 更新的回调,输入处理后的数据(value)对该条数据进行更新
* @param query 键(主键/索引)的值或者范围,如果是 IDBObjectStore 调用则是指定主键的值或者范围,如果是 IDBIndex 调用则是指定索引的值或者范围
* @param direction 游标方向,默认 'next'
* @param tx 事务
* @returns 无
*/
itorAll(
cb: (
key: IDBValidKey,
value: any,
update: (value: any) => Promise<IDBValidKey>
) => any,
query?: IDBValidKey | IDBKeyRange,
direction?: IDBCursorDirection,
tx?: IDBTransaction
) {
return this.exec<void>(tx, (store, resolve, reject) => {
const req = store.openCursor(query, direction);
req.onerror = () => {
reject(req.error);
};
req.onsuccess = async () => {
const cursor = req.result;
if (cursor) {
try {
let ret = cb(cursor.key, cursor.value, (value) => {
return new Promise<IDBValidKey>((resolve, reject) => {
const req1 = cursor.update(value);
handlRequest(req1, resolve, reject);
});
});
if (ret instanceof Promise) {
ret = await ret;
}
if (ret !== false) {
cursor.continue();
}
} catch (ex) {
reject(ex);
}
} else {
resolve();
}
};
});
}
/**
* 遍历所有 键(主键/索引)
* @param cb 针对每一条数据的回调处理函数
* @param primaryKey 主键的值
* @param key 键的值,如果是 IDBObjectStore 调用则是主键的值,如果是 IDBIndex 调用则是索引的值
* @param update 更新的回调,输入处理后的数据(value)对该条数据进行更新
* @param query 键(主键/索引)的值或者范围,如果是 IDBObjectStore 调用则是指定主键的值或者范围,如果是 IDBIndex 调用则是指定索引的值或者范围
* @param direction 游标方向,默认 'next'
* @param tx 事务
* @returns 无
*/
itorAllKeys(
cb: (
primaryKey: IDBValidKey,
key: IDBValidKey,
update: (value: any) => Promise<IDBValidKey>
) => any,
query?: IDBValidKey | IDBKeyRange,
direction?: IDBCursorDirection,
tx?: IDBTransaction
) {
return this.exec<void>(tx, (store, resolve, reject) => {
const req = store.openKeyCursor(query, direction);
req.onerror = () => {
reject(req.error);
};
req.onsuccess = async () => {
const cursor = req.result;
if (cursor) {
try {
let ret = cb(cursor.primaryKey, cursor.key, (value) => {
return new Promise<IDBValidKey>((resolve, reject) => {
const req1 = cursor.update(value);
handlRequest(req1, resolve, reject);
});
});
if (ret instanceof Promise) {
ret = await ret;
}
if (ret !== false) {
cursor.continue();
}
} catch (ex) {
reject(ex);
}
} else {
resolve();
}
};
});
}
count(query?: IDBValidKey | IDBKeyRange, tx?: IDBTransaction) {
return this.exec<number>(tx, (store, resolve, reject) => {
const req = store.count(query);
handlRequest(req, resolve, reject);
});
}
}
class SimpleStore extends AbstractSimpleStore<IDBObjectStore> {
constructor(readonly name: string, private readonly db: IDBDatabase) {
super();
}
exec<T>(
tx: IDBTransaction | undefined,
cb: SimpleStoreCallback<IDBObjectStore, T>
) {
return new Promise<T>((resolve, reject) => {
try {
tx = tx || this.db.transaction(this.name, "readwrite");
const store = tx.objectStore(this.name);
cb(store, resolve, reject);
} catch (ex) {
reject(ex);
}
});
}
getIndex(index: string) {
return new SimpleIndex(this.name, index, this.db);
}
put(key: IDBValidKey, value: any, tx?: IDBTransaction) {
return this.exec<any>(tx, (store, resolve, reject) => {
const req = store.put(value, key);
handlRequest(req, resolve, reject);
});
}
delete(key: IDBValidKey | IDBKeyRange, tx?: IDBTransaction) {
return this.exec<any>(tx, (store, resolve, reject) => {
const req = store.delete(key);
handlRequest(req, resolve, reject);
});
}
clear(tx?: IDBTransaction) {
return this.exec<boolean>(tx, (store, resolve, reject) => {
const req = store.clear();
req.onsuccess = () => {
resolve(true);
};
req.onerror = () => {
reject(new Error("Failed to clear data!"));
};
});
}
}
class SimpleIndex extends AbstractSimpleStore<IDBIndex> {
constructor(
readonly name: string,
readonly index: string,
private readonly db: IDBDatabase
) {
super();
}
exec<T>(
tx: IDBTransaction | undefined,
cb: SimpleStoreCallback<IDBIndex, T>
): Promise<T> {
return new Promise<T>((resolve, reject) => {
try {
tx = tx || this.db.transaction(this.name, "readwrite");
const store = tx.objectStore(this.name);
const index = store.index(this.index);
cb(index, resolve, reject);
} catch (ex) {
reject(ex);
}
});
}
}
type SimpleStoreCallback<T, V> = (
store: T,
resolve: (value: V) => void,
reject: (reason?: any) => void
) => void;
function handlRequest<T>(
req: IDBRequest<T>,
resolve: (value: T) => void,
reject: (reason?: any) => void
) {
req.onsuccess = () => {
resolve(req.result);
};
req.onerror = () => {
reject(req.error);
};
}
import { openSimpleDB } from "./SimpleDB";
import { sleep } from "./sleep";
export default async function testSimpleDB() {
console.log("test...", "testSimpleDB...");
const version = 5;
// 连接DB
let sdb = await openSimpleDB("test", version, (db) => {
console.log("upgrading...");
try {
db.deleteObjectStore("bbb");
} catch (error) {}
const store = db.createObjectStore("aaa");
store.createIndex("by_id", "id", { unique: true });
store.createIndex("by_name", "name");
});
console.log("IDBDatabase...", sdb);
try {
if (true) {
// 关闭DB连接
const closeReq = sdb.close();
console.log("close db...", closeReq);
// 删除DB
const delReq = await sdb.delete();
console.log("delete db...", delReq);
}
// 重连DB
sdb = await openSimpleDB("test", version + 1, (db) => {
console.log("upgrading...");
try {
db.deleteObjectStore("aaa");
} catch (error) {}
const store = db.createObjectStore("asdf");
store.createIndex("by_id", "id", { unique: true });
store.createIndex("by_name", "name");
// 生成复合索引
store.createIndex("id_name", ["id", "name"]);
});
console.log("IDBDatabase1...", sdb);
} catch (error) {
// 已经成功连接DB的情况下
// 直接迭代 DB 的版本,会失败, DB 处于 pending 状态 , 除非先关闭 DB 的连接
// 重连 DB , 版本不发生变化时,能成功连接
console.error("连接 DB 时 , 直接迭代 DB 版本失败...", error);
}
const asdf = sdb.store("asdf");
const idxId = asdf.getIndex("by_id");
const idxName = asdf.getIndex("by_name");
// 复合索引
const idxs = asdf.getIndex("id_name");
await asdf.put("1234", { id: "12341234", name: "asdf", date: new Date() });
await asdf.put("1234", { id: "12345555", name: "asdf", date: new Date() });
console.log(idxs.get(["12345555", "asdf"]));
try {
await asdf.put("4321", { id: "12341234", name: "asdf", date: new Date() });
} catch (error) {
// 约束了索引 id 是 unique 唯一索引,添加两个 id 一致的数据进入,则违反了约束,无法成功添加
console.error(
"Repeatedly add data that has agreed on a unique index...",
error
);
}
await sleep(100);
await asdf.put("1324", "acbd");
// 同一事务,处理多项操作
const tx = sdb.transaction(asdf);
await asdf.put("5678", "qqwer", tx);
await asdf.put("8765", "sdfwerf", tx);
// Promise.all 的性能优于 await 每一步操作完成
console.time("promise.all");
const tasks = [];
for (let i = 0; i < 100; i++) {
tasks.push(asdf.put("8765", "aasdfasdf", tx));
}
await Promise.all(tasks);
console.timeEnd("promise.all");
console.time("await everyone...");
for (let i = 0; i < 100; i++) {
await asdf.put("8765", "aasdfasdf", tx);
}
console.timeEnd("await everyone...");
// 休眠 1s 后,事务被提交,无法继续完成数据的处理
await sleep(1);
try {
await asdf.put("9988", "sadfwefwe", tx);
} catch (error) {
console.error("休眠 1s 后,transaction 被提交了,会报错...", error);
}
for (let i = 0; i < 10; i++) {
const tx1 = sdb.transaction(asdf);
console.log(await asdf.get("8765", tx1));
await asdf.delete("8765", tx1);
tx1.abort(); // 放弃本次连接的事务的所有修改,若当前的事务处于回滚或完成状态时,则会抛出一个错误事件
}
console.log(await asdf.get("8765"));
await asdf.delete("8765");
console.log(await asdf.get("8765"));
const tx2 = sdb.transaction(asdf);
console.log(await asdf.get("5678", tx2));
await asdf.delete("5678", tx2);
tx2.commit(); // 提交事务
try {
tx2.abort(); // 放弃本次连接的事务的所有修改,若当前的事务处于回滚或完成状态时,则会抛出一个错误事件
console.log("transaction abort...");
} catch (error) {
console.error("transaction abort failed...", error);
}
console.log(await asdf.get("5678"));
/*---------------------------------- */
await asdf.put("55555555.1", {
id: "55555555",
name: "asdf",
date: new Date(),
});
await asdf.put("66666666.1", {
id: "66666666",
name: "asdf",
date: new Date(),
});
console.log("asdf itorAll");
await asdf.itorAll(console.log);
console.log("asdf itorAll update");
await asdf.itorAll((key, value, update: Function) => {
console.log(key, value);
if (key === "55555555.1") {
console.log("itorAll update...");
update({ id: "55555555", name: "itorAll update..." });
}
});
console.log(await asdf.get("55555555.1"));
console.log("itorAll update end...");
console.log("asdf itorAllKeys");
await asdf.itorAllKeys(console.log);
console.log("asdf itorAllKeys update");
await asdf.itorAllKeys((primaryKey, key, update: Function) => {
console.log(primaryKey, key);
if (primaryKey === "55555555.1") {
console.log("itorAllKeys update...");
update({ id: "55555555", name: "itorAllKeys update..." });
}
});
console.log(await asdf.get("55555555.1"));
console.log("itorAllKeys update end...");
/*---------------------------------- */
console.log("idxId itorAll");
await idxId.itorAll(console.log, "55555555"); // 指定了索引的值
console.log("idxId itorAllKeys");
await idxId.itorAllKeys(
console.log,
IDBKeyRange.bound("55555555", "66666666", false, true)
); // 指定了索引的范围
/*---------------------------------- */
console.log("idxName itorAll");
await idxName.itorAll(console.log);
console.log("idxName itorAllKeys");
await idxName.itorAllKeys(console.log);
/*---------------------------------- */
console.log("asdf getAll");
console.log(await asdf.getAll());
console.log(await asdf.getAll(IDBKeyRange.bound("1234", "4321"), 3));
console.log("asdf getAllKeys");
console.log(await asdf.getAllKeys());
console.log(await asdf.getAllKeys(IDBKeyRange.bound("1234", "4321"), 3));
console.log("asdf getKey");
console.log(await asdf.getKey(IDBKeyRange.bound("1234", "4321")));
/*---------------------------------- */
console.log("idxId getAll");
console.log(await idxId.getAll());
console.log(
await idxId.getAll(
IDBKeyRange.bound("55555555", "66666666", false, true),
3
)
);
console.log("idxId getAllKeys");
console.log(await idxId.getAllKeys());
console.log(
await idxId.getAllKeys(
IDBKeyRange.bound("55555555", "66666666", false, false),
3
)
);
console.log("idxId getKey");
console.log(
await idxId.getKey(IDBKeyRange.bound("55555555", "66666666", false, false))
);
/*---------------------------------- */
console.log("asdf count");
console.log(await asdf.count());
console.log(await asdf.count(IDBKeyRange.bound("1234", "4321")));
/*---------------------------------- */
console.log("idxId count");
console.log(await idxId.count());
console.log(
await idxId.count(IDBKeyRange.bound("55555555", "66666666", false, true))
);
/*---------------------------------- */
console.log("asdf clear");
console.log(await asdf.clear());
}