UNPKG

@atomiqlabs/sdk-lib

Version:

Basic SDK functionality library for atomiq

300 lines (253 loc) 13 kB
import {getLogger, LoggerType} from "../utils/Utils"; import {IUnifiedStorage, QueryParams, UnifiedStoredObject} from "../storage/IUnifiedStorage"; import {ISwap} from "../swaps/ISwap"; import {SwapType} from "../swaps/SwapType"; export type QuerySetCondition = { key: string, values: Set<any> } function toCompositeIndex(values: Array<any[]>): Array<any[]> { if(values.length===0) return []; if(values.length===1) { return values[0]; } else { const compositeArray = []; const firstValues = values.shift(); const restValues = toCompositeIndex(values); for(let value of firstValues) { for(let restValue of restValues) { compositeArray.push([value].concat(restValue)); } } return compositeArray; } } function matches(conditions: Array<QuerySetCondition>, obj: any): boolean { for(let condition of conditions) { let value = obj[condition.key]; if(!condition.values.has(value)) return false; } return true; } function toSetConditions(input: Array<QueryParams>): Array<QuerySetCondition> { return input.map(val => { return { key: val.key, values: Array.isArray(val.value) ? new Set(val.value) : new Set([val.value]) }; }) } const indexes = { "escrowHash": { key: "escrowHash", unique: true}, "type": {key: "type", unique: false}, "initiator": {key: "initiator", unique: false}, "initiator, id": {key: ["initiator", "id"], unique: false}, "type, state": {key: ["type", "state"], unique: false}, "type, paymentHash": {key: ["type", "paymentHash"], unique: false}, "type, initiator, state": {key: ["type", "initiator", "state"], unique: false} } export class IndexedDBUnifiedStorage implements IUnifiedStorage { protected readonly logger: LoggerType; storageKey: string; db: IDBDatabase; constructor(storageKey: string) { this.storageKey = storageKey; this.logger = getLogger("IndexedDBUnifiedStorage("+this.storageKey+"): "); } //Reviver also needs to update the swap to the latest version private async tryMigrateLocalStorage(storageKey: string, swapType: SwapType, reviver: (obj: any) => ISwap): Promise<boolean> { const txt = window.localStorage.getItem(storageKey); if(txt==null) return false; let data: {[key: string]: any}; try { data = JSON.parse(txt); } catch (e) { this.logger.warn("tryMigrate("+storageKey+"): Tried to migrate the database, but cannot parse old local storage!"); return false; } let swaps: ISwap[] = Object.keys(data).map(id => { let swapData = data[id]; swapData.type = swapType; return reviver(swapData); }); await this.saveAll(swaps.map(swap => swap.serialize())); window.localStorage.removeItem(storageKey); this.logger.info("tryMigrate("+storageKey+"): Database successfully migrated from localStorage to unifiedIndexedDB!"); return true; } //Reviver also needs to update the swap to the latest version private async tryMigrateOldIndexedDB(storageKey: string, swapType: SwapType, reviver: (obj: any) => ISwap): Promise<boolean> { const databases = await window.indexedDB.databases(); if(databases.find(val => val.name===storageKey)==null) { this.logger.info("tryMigrateOldIndexedDB("+storageKey+"): Old database not found!"); return false; } this.logger.debug("tryMigrateOldIndexedDB("+storageKey+"): Old database found!"); let db: IDBDatabase; try { db = await new Promise<IDBDatabase>((resolve, reject) => { const request = window.indexedDB.open(storageKey, 1); request.onerror = (e) => reject(e); request.onsuccess = (e: any) => resolve(e.target.result); }); } catch (e) { this.logger.warn("tryMigrateOldIndexedDB("+storageKey+"): Error opening old IndexedDB!", e); return false; } this.logger.debug("tryMigrateOldIndexedDB("+storageKey+"): Connection opened!"); try { const data = await new Promise<{ id: string, data: any }[]>((resolve, reject) => { const tx = db.transaction("swaps", "readonly", {durability: "strict"}); const store = tx.objectStore("swaps"); const req = store.getAll(); req.onsuccess = (event: any) => resolve(event.target.result); req.onerror = (event) => reject(event); }); this.logger.debug("tryMigrateOldIndexedDB("+storageKey+"): Data retrieved!"); let swaps: ISwap[] = data.map(({id, data}) => { data.type = swapType; return reviver(data); }); this.logger.debug("tryMigrateOldIndexedDB("+storageKey+"): Data revived!"); await this.saveAll(swaps.map(swap => swap.serialize())); this.logger.debug("tryMigrateOldIndexedDB("+storageKey+"): Data saved!"); //Remove the old database db.close(); this.logger.debug("tryMigrateOldIndexedDB("+storageKey+"): DB connection closed!"); await new Promise<void>((resolve, reject) => { const res = window.indexedDB.deleteDatabase(storageKey); res.onsuccess = () => resolve(); res.onerror = (e) => reject(e); }); this.logger.info("tryMigrateOldIndexedDB("+storageKey+"): Database successfully migrated from oldIndexedDB to unifiedIndexedDB!"); return true; } catch (e) { this.logger.warn("tryMigrateOldIndexedDB("+storageKey+"): Tried to migrate the database, but cannot parse oldIndexedDB!", e); return false; } } //NOTE: Reviver also needs to update the swap to the latest version public async tryMigrate(storageKeys: [string, SwapType][], reviver: (obj: any) => ISwap): Promise<boolean> { let someMigrated = false; for(let storageKey of storageKeys) { this.logger.info("tryMigrate(): Trying to migrate...", storageKey); if(await this.tryMigrateLocalStorage(storageKey[0], storageKey[1], reviver)) someMigrated = true; if(await this.tryMigrateOldIndexedDB(storageKey[0], storageKey[1], reviver)) someMigrated = true; } return someMigrated; } private executeTransaction<T>(cbk: (tx: IDBObjectStore) => IDBRequest<T>, readonly: boolean): Promise<T> { return new Promise<T>((resolve, reject) => { const tx = this.db.transaction("swaps", readonly ? "readonly" : "readwrite", {durability: "strict"}); const req = cbk(tx.objectStore("swaps")); req.onsuccess = (event: any) => resolve(event.target.result); req.onerror = (event) => reject(event); }); } private executeTransactionArr<T>(cbk: (tx: IDBObjectStore) => IDBRequest<T>[], readonly: boolean): Promise<T[]> { const tx = this.db.transaction("swaps", readonly ? "readonly" : "readwrite", {durability: "strict"}); const reqs = cbk(tx.objectStore("swaps")); return Promise.all(reqs.map(req => new Promise<T>((resolve, reject) => { req.onsuccess = (event: any) => resolve(event.target.result); req.onerror = (event) => reject(event); }))); } private executeTransactionWithCursor<T>(cbk: (tx: IDBObjectStore) => IDBRequest<IDBCursorWithValue>[], valueCbk: (value: T) => boolean): Promise<T[]> { return new Promise<T[]>((resolve, reject) => { const tx = this.db.transaction("swaps", "readonly", {durability: "strict"}); const cursorRequests = cbk(tx.objectStore("swaps")); const resultObjects: T[] = []; for(let cursorRequest of cursorRequests) { cursorRequest.onsuccess = (event: any) => { const cursor = event.target.result; if(cursor!=null) { const value = cursor.value; if(valueCbk(value)) resultObjects.push(value); cursor.continue(); } else { resolve(resultObjects); } } cursorRequest.onerror = (event) => reject(event); } }); } async init(): Promise<void> { if(this.db==null) { this.db = await new Promise<IDBDatabase>((resolve, reject) => { const request = window.indexedDB.open(this.storageKey, 1); request.onupgradeneeded = (event: any) => { const db: IDBDatabase = event.target.result; const objectStore = db.createObjectStore("swaps", { keyPath: "id" }); Object.keys(indexes).forEach(name => { const index = indexes[name]; objectStore.createIndex(name, index.key, {unique: index.unique}); }) }; request.onerror = (e) => reject(e); request.onsuccess = (e: any) => resolve(e.target.result); }); } } /** * Params are specified in the following way: * - [[condition1, condition2]] - returns all rows where condition1 AND condition2 is met * - [[condition1], [condition2]] - returns all rows where condition1 OR condition2 is met * - [[condition1, condition2], [condition3]] - returns all rows where (condition1 AND condition2) OR condition3 is met * @param params */ async query(params: Array<Array<QueryParams>>): Promise<Array<UnifiedStoredObject>> { if(params.length===0) return await this.querySingle([]); const results = await Promise.all(params.map(singleParam => this.querySingle(singleParam))); const resultSet = new Set(results.flat()); //Deduplicate return Array.from(resultSet); } async querySingle(params: Array<QueryParams>): Promise<Array<UnifiedStoredObject>> { if(params.length===0) { return await this.executeTransaction((objectStore) => objectStore.getAll(), true); } const queryKeys = params.map(param => param.key); const requiredIndex = queryKeys.join(", "); if(requiredIndex==="id") { //ID is the index const values: any[] = Array.isArray(params[0].value) ? params[0].value : [params[0].value]; const res = await this.executeTransactionArr((objectStore) => { return values.map(val => objectStore.getAll(val)); }, true); return res.flat(); } else if(indexes[requiredIndex]!=null) { //Index exists const values: Array<any[]> = params.map(param => Array.isArray(param.value) ? param.value : [param.value]); const compositeIndexQueries = toCompositeIndex(values); const resp = await this.executeTransactionArr(objectStore => { const index = objectStore.index(requiredIndex); return compositeIndexQueries.map(indexQuery => index.getAll(indexQuery)); }, true); return resp.flat(); } else { //Need to go over all values this.logger.warn("query(): Index cannot be used for query, required index: "+requiredIndex+" query params: ", params); const setConditions = toSetConditions(params); return await this.executeTransactionWithCursor(objectStore => [objectStore.openCursor()], (val: any) => matches(setConditions, val)); } } async remove(object: UnifiedStoredObject): Promise<void> { await this.executeTransaction<undefined>(store => store.delete(object.id), false) .catch(() => null); } async removeAll(arr: UnifiedStoredObject[]): Promise<void> { if(arr.length===0) return; await this.executeTransactionArr<IDBValidKey>(store => arr.map(object => { return store.delete(object.id); }), false); } async save(object: UnifiedStoredObject): Promise<void> { await this.executeTransaction<IDBValidKey>(store => store.put(object), false); } async saveAll(arr: UnifiedStoredObject[]): Promise<void> { if(arr.length===0) return; await this.executeTransactionArr<IDBValidKey>(store => arr.map(object => { return store.put(object); }), false); } }