tweb-i2p/src/lib/storage.ts

246 lines
7.1 KiB
TypeScript
Raw Normal View History

2021-04-08 17:52:31 +04:00
/*
* 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
*/
2021-02-13 19:32:10 +04:00
import { DatabaseStore, DatabaseStoreName } from "../config/database";
2021-04-28 21:47:57 +04:00
import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise";
import { throttle } from "../helpers/schedulers";
import IDBStorage, { IDBOptions } from "./idb";
function noop() {}
export default class AppStorage<Storage extends Record<string, any>/* Storage extends {[name: string]: any} *//* Storage extends Record<string, any> */> {
2021-04-24 21:06:24 +04:00
private static STORAGES: AppStorage<any>[] = [];
private storage: IDBStorage;//new CacheStorageController('session');
//private cache: Partial<{[key: string]: Storage[typeof key]}> = {};
private cache: Partial<Storage> = {};
private useStorage = true;
2021-04-28 21:47:57 +04:00
private getPromises: Map<keyof Storage, CancellablePromise<Storage[keyof Storage]>> = new Map();
private getThrottled: () => void;
2021-04-27 19:45:53 +04:00
private keysToSet: Set<keyof Storage> = new Set();
private saveThrottled: () => void;
private saveDeferred = deferredPromise<void>();
private keysToDelete: Set<keyof Storage> = new Set();
private deleteThrottled: () => void;
private deleteDeferred = deferredPromise<void>();
constructor(storageOptions: Omit<IDBOptions, 'storeName' | 'stores'> & {stores?: DatabaseStore[], storeName: DatabaseStoreName}) {
this.storage = new IDBStorage(storageOptions);
AppStorage.STORAGES.push(this);
this.saveThrottled = throttle(async() => {
const deferred = this.saveDeferred;
this.saveDeferred = deferredPromise<void>();
const set = this.keysToSet;
if(set.size) {
const keys = Array.from(set.values()) as string[];
set.clear();
2021-04-28 21:47:57 +04:00
try {
//console.log('setItem: will set', key/* , value */);
//await this.cacheStorage.delete(key); // * try to prevent memory leak in Chrome leading to 'Unexpected internal error.'
//await this.storage.save(key, new Response(value, {headers: {'Content-Type': 'application/json'}}));
await this.storage.save(keys, keys.map(key => this.cache[key]));
//console.log('setItem: have set', key/* , value */);
} catch(e) {
//this.useCS = false;
console.error('[AS]: set error:', e, keys/* , value */);
}
2021-04-27 19:45:53 +04:00
}
deferred.resolve();
if(set.size) {
this.saveThrottled();
}
2021-04-28 21:47:57 +04:00
}, 16, false);
this.deleteThrottled = throttle(async() => {
const deferred = this.deleteDeferred;
this.deleteDeferred = deferredPromise<void>();
const set = this.keysToDelete;
if(set.size) {
const keys = Array.from(set.values()) as string[];
set.clear();
try {
await this.storage.delete(keys);
} catch(e) {
console.error('[AS]: delete error:', e, keys);
}
}
deferred.resolve();
if(set.size) {
this.deleteThrottled();
}
}, 16, false);
2021-04-28 21:47:57 +04:00
this.getThrottled = throttle(async() => {
const keys = Array.from(this.getPromises.keys());
this.storage.get(keys as string[]).then(values => {
for(let i = 0, length = keys.length; i < length; ++i) {
const key = keys[i];
const deferred = this.getPromises.get(key);
if(deferred) {
// @ts-ignore
deferred.resolve(this.cache[key] = values[i]);
this.getPromises.delete(key);
}
}
}, (error) => {
if(!['NO_ENTRY_FOUND', 'STORAGE_OFFLINE'].includes(error)) {
this.useStorage = false;
console.error('[AS]: get error:', error, keys, storageOptions.storeName);
2021-04-28 21:47:57 +04:00
}
for(let i = 0, length = keys.length; i < length; ++i) {
const key = keys[i];
const deferred = this.getPromises.get(key);
if(deferred) {
//deferred.reject(error);
deferred.resolve();
2021-04-28 21:47:57 +04:00
this.getPromises.delete(key);
}
}
}).finally(() => {
if(this.getPromises.size) {
this.getThrottled();
}
2021-04-28 21:47:57 +04:00
});
}, 16, false);
}
public isAvailable() {
return this.useStorage;
}
public getCache() {
return this.cache;
}
public getFromCache(key: keyof Storage) {
return this.cache[key];
}
public setToCache(key: keyof Storage, value: Storage[typeof key]) {
return this.cache[key] = value;
}
public async get(key: keyof Storage): Promise<Storage[typeof key]> {
if(this.cache.hasOwnProperty(key)) {
return this.getFromCache(key);
} else if(this.useStorage) {
2021-04-28 21:47:57 +04:00
const r = this.getPromises.get(key);
if(r) return r;
const p = deferredPromise<Storage[typeof key]>();
this.getPromises.set(key, p);
2021-04-28 21:47:57 +04:00
this.getThrottled();
return p;
2021-04-23 20:29:49 +04:00
}/* else {
throw 'something went wrong';
2021-04-23 20:29:49 +04:00
} */
}
2021-04-28 15:08:14 +04:00
public getAll() {
return this.storage.getAll().catch(() => []);
2021-04-28 15:08:14 +04:00
}
2021-04-28 21:47:57 +04:00
public set(obj: Partial<Storage>, onlyLocal = false) {
//console.log('storageSetValue', obj, callback, arguments);
for(const key in obj) {
if(obj.hasOwnProperty(key)) {
const value = obj[key];
this.setToCache(key, value);
// let perf = /* DEBUG */false ? performance.now() : 0;
// value = JSON.stringify(value);
// if(perf) {
// let elapsedTime = performance.now() - perf;
// if(elapsedTime > 10) {
// console.warn('LocalStorage set: stringify time by JSON.stringify:', elapsedTime, key);
// }
// }
/* perf = performance.now();
value = stringify(value);
2020-10-17 01:31:58 +03:00
console.log('LocalStorage set: stringify time by own stringify:', performance.now() - perf); */
if(this.useStorage && !onlyLocal) {
2021-04-27 19:45:53 +04:00
this.keysToSet.add(key);
this.keysToDelete.delete(key);
this.saveThrottled();
}
}
}
2021-04-28 21:47:57 +04:00
return this.useStorage ? this.saveDeferred : Promise.resolve();
}
public delete(key: keyof Storage, saveLocal = false) {
/* if(!this.cache.hasOwnProperty(key)) {
return;
} */
2021-04-27 19:45:53 +04:00
// ! it is needed here
key = '' + key;
if(!saveLocal) {
delete this.cache[key];
}
if(this.useStorage) {
2021-04-27 19:45:53 +04:00
this.keysToSet.delete(key);
this.keysToDelete.add(key);
this.deleteThrottled();
}
return this.useStorage ? this.deleteDeferred : Promise.resolve();
}
public clear() {
return this.storage.deleteAll().catch(noop);
}
public static toggleStorage(enabled: boolean) {
return Promise.all(this.STORAGES.map(storage => {
storage.useStorage = enabled;
if(!enabled) {
2021-04-27 19:45:53 +04:00
storage.keysToSet.clear();
storage.keysToDelete.clear();
2021-04-28 21:47:57 +04:00
storage.getPromises.forEach((deferred) => deferred.resolve());
storage.getPromises.clear();
return storage.clear();
} else {
return storage.set(storage.cache);
}
})).catch(noop);
}
public deleteDatabase() {
return IDBStorage.deleteDatabase().catch(noop);
}
}