Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

469 lines
13 KiB

3 years ago
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*
* Originally from:
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import { Database } from '../config/databases';
import Modes from '../config/modes';
import blobConstruct from '../helpers/blob/blobConstruct';
import safeAssign from '../helpers/object/safeAssign';
3 years ago
import { logger } from './logger';
/**
* https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/createIndex
*/
export type IDBIndex = {
indexName: string,
keyPath: string,
objectParameters: IDBIndexParameters
};
export type IDBStore = {
name: string,
indexes?: IDBIndex[]
};
export type IDBOptions = {
name?: string,
storeName: string,
stores?: IDBStore[],
version?: number
};
const DEBUG = false;
export default class IDBStorage<T extends Database<any>> {
private static STORAGES: IDBStorage<Database<any>>[] = [];
private openDbPromise: Promise<IDBDatabase>;
private db: IDBDatabase;
private storageIsAvailable = true;
private log: ReturnType<typeof logger>;
private name: string;
private version: number;
private stores: IDBStore[];
private storeName: T['stores'][0]['name'];
constructor(db: T, storeName: typeof db['stores'][0]['name']) {
safeAssign(this, db);
if(Modes.test) {
this.name += '_test';
}
this.storeName = storeName;
this.log = logger('IDB-' + this.storeName);
this.openDatabase(true);
IDBStorage.STORAGES.push(this);
}
public static closeDatabases(preserve?: IDBStorage<Database<any>>) {
this.STORAGES.forEach(storage => {
if(preserve && preserve === storage) {
return;
}
const db = storage.db;
if(db) {
db.onclose = () => {};
db.close();
}
});
}
/**
* ! WARNING ! function requires at least one opened connection
*/
/* public static clearObjectStores() {
const storage = this.STORAGES[0];
this.closeDatabases(storage);
const names = Array.from(storage.db.objectStoreNames);
const promises = names.map(name => storage.clear(name));
return Promise.all(promises);
} */
/* public static deleteDatabase() {
this.closeDatabases();
const storages = this.STORAGES;
const dbNames = Array.from(new Set(storages.map(storage => storage.name)));
const promises = dbNames.map(dbName => {
return new Promise<void>((resolve, reject) => {
const deleteRequest = indexedDB.deleteDatabase(dbName);
deleteRequest.onerror = () => {
reject();
};
deleteRequest.onsuccess = () => {
resolve();
};
});
});
return Promise.all(promises);
} */
public isAvailable() {
return this.storageIsAvailable;
}
public openDatabase(createNew = false): Promise<IDBDatabase> {
if(this.openDbPromise && !createNew) {
return this.openDbPromise;
}
const createObjectStore = (db: IDBDatabase, store: IDBStore) => {
const os = db.createObjectStore(store.name);
if(store.indexes?.length) {
for(const index of store.indexes) {
os.createIndex(index.indexName, index.keyPath, index.objectParameters);
}
}
};
try {
var request = indexedDB.open(this.name, this.version);
if(!request) {
return Promise.reject();
}
} catch(error) {
this.log.error('error opening db', (error as Error).message);
3 years ago
this.storageIsAvailable = false;
return Promise.reject(error);
}
let finished = false;
setTimeout(() => {
if(!finished) {
request.onerror({type: 'IDB_CREATE_TIMEOUT'} as Event);
}
}, 3000);
return this.openDbPromise = new Promise<IDBDatabase>((resolve, reject) => {
request.onsuccess = (event) => {
finished = true;
const db = request.result;
let calledNew = false;
this.log('Opened');
db.onerror = (error) => {
this.storageIsAvailable = false;
this.log.error('Error creating/accessing IndexedDB database', error);
reject(error);
};
db.onclose = (e) => {
this.log.error('closed:', e);
!calledNew && this.openDatabase();
};
db.onabort = (e) => {
this.log.error('abort:', e);
const transaction = e.target as IDBTransaction;
this.openDatabase(calledNew = true);
if(transaction.onerror) {
transaction.onerror(e);
}
db.close();
};
db.onversionchange = (e) => {
this.log.error('onversionchange, lol?');
};
resolve(this.db = db);
};
request.onerror = (event) => {
finished = true;
this.storageIsAvailable = false;
this.log.error('Error creating/accessing IndexedDB database', event);
reject(event);
};
request.onupgradeneeded = (event) => {
finished = true;
this.log.warn('performing idb upgrade from', event.oldVersion, 'to', event.newVersion);
// @ts-ignore
var db = event.target.result as IDBDatabase;
this.stores.forEach((store) => {
/* if(db.objectStoreNames.contains(store.name)) {
//if(event.oldVersion === 1) {
db.deleteObjectStore(store.name);
//}
} */
if(!db.objectStoreNames.contains(store.name)) {
createObjectStore(db, store);
}
});
};
});
}
public delete(entryName: string | string[]): Promise<void> {
//return Promise.resolve();
if(!Array.isArray(entryName)) {
entryName = [].concat(entryName);
}
return this.getObjectStore('readwrite', (objectStore) => {
return (entryName as string[]).map((entryName) => objectStore.delete(entryName));
}, DEBUG ? 'delete: ' + entryName.join(', ') : '');
}
public clear(storeName?: IDBStorage<T>['storeName']) {
return this.getObjectStore('readwrite', (objectStore) => objectStore.clear(), DEBUG ? 'clear' : '', storeName);
}
public save(entryName: string | string[], value: any | any[]) {
// const handleError = (error: Error) => {
// this.log.error('save: transaction error:', entryName, value, db, error, error && error.name);
// if((!error || error.name === 'InvalidStateError')/* && false */) {
// setTimeout(() => {
// this.save(entryName, value);
// }, 2e3);
// } else {
// //console.error('IndexedDB saveFile transaction error:', error, error && error.name);
// }
// };
if(!Array.isArray(entryName)) {
entryName = [].concat(entryName);
value = [].concat(value);
}
return this.getObjectStore('readwrite', (objectStore) => {
return (entryName as string[]).map((entryName, idx) => objectStore.put(value[idx], entryName));
}, DEBUG ? 'save: ' + entryName.join(', ') : '');
}
public saveFile(fileName: string, blob: Blob | Uint8Array) {
//return Promise.resolve(blobConstruct([blob]));
if(!(blob instanceof Blob)) {
blob = blobConstruct([blob]) as Blob;
}
return this.save(fileName, blob);
}
/* public saveFileBase64(db: IDBDatabase, fileName: string, blob: Blob | any): Promise<Blob> {
if(this.getBlobSize(blob) > 10 * 1024 * 1024) {
return Promise.reject();
}
if(!(blob instanceof Blob)) {
var safeMimeType = blobSafeMimeType(blob.type || 'image/jpeg');
var address = 'data:' + safeMimeType + ';base64,' + bytesToBase64(blob);
return this.storagePutB64String(db, fileName, address).then(() => {
return blob;
});
}
try {
var reader = new FileReader();
} catch (e) {
this.storageIsAvailable = false;
return Promise.reject();
}
let promise = new Promise<Blob>((resolve, reject) => {
reader.onloadend = () => {
this.storagePutB64String(db, fileName, reader.result as string).then(() => {
resolve(blob);
}, reject);
}
reader.onerror = reject;
});
try {
reader.readAsDataURL(blob);
} catch (e) {
this.storageIsAvailable = false;
return Promise.reject();
}
return promise;
}
public storagePutB64String(db: IDBDatabase, fileName: string, b64string: string) {
try {
var objectStore = db.transaction([this.storeName], 'readwrite')
.objectStore(this.storeName);
var request = objectStore.put(b64string, fileName);
} catch(error) {
this.storageIsAvailable = false;
return Promise.reject(error);
}
return new Promise((resolve, reject) => {
request.onsuccess = function(event) {
resolve();
};
request.onerror = reject;
});
}
public getBlobSize(blob: any) {
return blob.size || blob.byteLength || blob.length;
} */
public get<T>(entryName: string[]): Promise<T[]>;
public get<T>(entryName: string): Promise<T>;
public get<T>(entryName: string | string[]): Promise<T> | Promise<T[]> {
//return Promise.reject();
if(!Array.isArray(entryName)) {
entryName = [].concat(entryName);
}
return this.getObjectStore<T>('readonly', (objectStore) => {
return (entryName as string[]).map((entryName) => objectStore.get(entryName));
}, DEBUG ? 'get: ' + entryName.join(', ') : '');
}
private getObjectStore<T>(mode: IDBTransactionMode, objectStore: (objectStore: IDBObjectStore) => IDBRequest | IDBRequest[], log?: string, storeName = this.storeName) {
let perf: number;
if(log) {
perf = performance.now();
this.log(log + ': start');
}
return this.openDatabase().then((db) => {
return new Promise<T>((resolve, reject) => {
/* if(mode === 'readwrite') {
return;
} */
const transaction = db.transaction([storeName], mode);
transaction.onerror = (e) => {
clearTimeout(timeout);
reject(transaction.error);
};
transaction.oncomplete = (e) => {
clearTimeout(timeout);
if(log) {
this.log(log + ': end', performance.now() - perf);
}
const results = r.map(r => r.result);
resolve(isArray ? results : results[0]);
};
const timeout = setTimeout(() => {
this.log.error('transaction not finished', transaction);
}, 10000);
/* transaction.addEventListener('abort', (e) => {
//handleError();
this.log.error('IndexedDB: transaction abort!', transaction.error);
}); */
const requests = objectStore(transaction.objectStore(storeName));
const isArray = Array.isArray(requests);
const r: IDBRequest[] = isArray ? requests : [].concat(requests) as any;
// const length = r.length;
// /* let left = length;
// const onRequestFinished = (error?: Error) => {
// if(!--left) {
// resolve(result);
// clearTimeout(timeout);
// }
// }; */
// for(let i = 0; i < length; ++i) {
// const request = r[i];
// request.onsuccess = () => {
// onRequestFinished();
// };
// request.onerror = (e) => {
// onRequestFinished(transaction.error);
// };
// }
});
});
}
public getAll<T>(): Promise<T[]> {
return this.getObjectStore<T[]>('readonly', (objectStore) => objectStore.getAll(), DEBUG ? 'getAll' : '');
}
/* public getAllKeys(): Promise<Array<string>> {
console.time('getAllEntries');
return this.openDatabase().then((db) => {
var objectStore = db.transaction([this.storeName], 'readonly')
.objectStore(this.storeName);
var request = objectStore.getAllKeys();
return new Promise((resolve, reject) => {
request.onsuccess = function(event) {
// @ts-ignore
var result = event.target.result;
resolve(result);
console.timeEnd('getAllEntries');
}
request.onerror = reject;
});
});
} */
/* public isFileExists(fileName: string): Promise<boolean> {
console.time('isFileExists');
return this.openDatabase().then((db) => {
var objectStore = db.transaction([this.storeName], 'readonly')
.objectStore(this.storeName);
var request = objectStore.openCursor(fileName);
return new Promise((resolve, reject) => {
request.onsuccess = function(event) {
// @ts-ignore
var cursor = event.target.result;
resolve(!!cursor);
console.timeEnd('isFileExists');
}
request.onerror = reject;
});
});
} */
/* public getFileWriter(fileName: string, mimeType: string) {
var fakeWriter = FileManager.getFakeFileWriter(mimeType, (blob) => {
return this.saveFile(fileName, blob);
});
return Promise.resolve(fakeWriter);
} */
}