From 64efb48923f665909f0ec17d76fb502be96ead85 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Wed, 28 Apr 2021 21:47:57 +0400 Subject: [PATCH] beta state fix --- src/helpers/object.ts | 13 +- src/lib/appManagers/appChatsManager.ts | 25 +- src/lib/appManagers/appDialogsManager.ts | 4 +- src/lib/appManagers/appDraftsManager.ts | 10 +- src/lib/appManagers/appStateManager.ts | 198 +++++++++------ src/lib/appManagers/appUsersManager.ts | 25 +- src/lib/idb.ts | 293 +++++++---------------- src/lib/mtproto/mtprotoworker.ts | 2 + src/lib/sessionStorage.ts | 4 +- src/lib/storage.ts | 97 +++++--- src/lib/storages/dialogs.ts | 119 +++++---- src/lib/storages/filters.ts | 6 +- 12 files changed, 386 insertions(+), 410 deletions(-) diff --git a/src/helpers/object.ts b/src/helpers/object.ts index 77661a5d..aa507a52 100644 --- a/src/helpers/object.ts +++ b/src/helpers/object.ts @@ -132,12 +132,13 @@ export function setDeepProperty(object: any, key: string, value: any) { getDeepProperty(object, splitted.slice(0, -1).join('.'))[splitted.pop()] = value; } -export function validateInitObject(initObject: any, currentObject: any) { - for(const i in initObject) { - if(typeof(currentObject[i]) !== typeof(initObject[i])) { - currentObject[i] = copy(initObject[i]); - } else if(isObject(initObject[i])) { - validateInitObject(initObject[i], currentObject[i]); +export function validateInitObject(initObject: any, currentObject: any, onReplace?: (key: string) => void, previousKey?: string) { + for(const key in initObject) { + if(typeof(currentObject[key]) !== typeof(initObject[key])) { + currentObject[key] = copy(initObject[key]); + onReplace && onReplace(previousKey || key); + } else if(isObject(initObject[key])) { + validateInitObject(initObject[key], currentObject[key], onReplace, previousKey || key); } } } diff --git a/src/lib/appManagers/appChatsManager.ts b/src/lib/appManagers/appChatsManager.ts index b980b2ec..9af2a1f5 100644 --- a/src/lib/appManagers/appChatsManager.ts +++ b/src/lib/appManagers/appChatsManager.ts @@ -18,7 +18,6 @@ import apiManagerProxy from "../mtproto/mtprotoworker"; import apiManager from '../mtproto/mtprotoworker'; import { RichTextProcessor } from "../richtextprocessor"; import rootScope from "../rootScope"; -import AppStorage from "../storage"; import apiUpdatesManager from "./apiUpdatesManager"; import appMessagesManager from "./appMessagesManager"; import appPeersManager from "./appPeersManager"; @@ -33,9 +32,7 @@ export type ChatRights = keyof ChatBannedRights['pFlags'] | keyof ChatAdminRight export type UserTyping = Partial<{userId: number, action: SendMessageAction, timeout: number}>; export class AppChatsManager { - private storage = new AppStorage>({ - storeName: 'chats' - }); + private storage = appStateManager.storages.chats; private chats: {[id: number]: Chat.channel | Chat.chat | any} = {}; //private usernames: any = {}; @@ -74,20 +71,16 @@ export class AppChatsManager { updateChannelUserTyping: this.onUpdateUserTyping }); - let storageChats: Chat[]; - const getStorageChatsPromise = this.storage.getAll().then(chats => { - storageChats = chats as any; - }); - - appStateManager.addLoadPromise(getStorageChatsPromise).then((state) => { - if(storageChats.length) { + appStateManager.getState().then((state) => { + const chats = appStateManager.storagesResults.chats; + if(chats.length) { this.chats = {}; - for(let i = 0, length = storageChats.length; i < length; ++i) { - const user = storageChats[i]; - this.chats[user.id] = user; + for(let i = 0, length = chats.length; i < length; ++i) { + const chat = chats[i]; + if(chat) { + this.chats[chat.id] = chat; + } } - } else if(state.chats) { - this.chats = state.chats; } appStateManager.addEventListener('peerNeeded', (peerId: number) => { diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index 6dfe205c..2d92f3ce 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -500,13 +500,15 @@ export class AppDialogsManager { } }); - if(state.dialogs?.length) { + if(appStateManager.storagesResults.dialogs.length) { appDraftsManager.getAllDrafts(); appDraftsManager.addMissedDialogs(); } return this.loadDialogs(); }).then(() => { + return; + const isLoadedMain = appMessagesManager.dialogsStorage.isDialogsLoaded(0); const isLoadedArchive = appMessagesManager.dialogsStorage.isDialogsLoaded(1); const wasLoaded = isLoadedMain || isLoadedArchive; diff --git a/src/lib/appManagers/appDraftsManager.ts b/src/lib/appManagers/appDraftsManager.ts index 935c00f7..6c0d1f3e 100644 --- a/src/lib/appManagers/appDraftsManager.ts +++ b/src/lib/appManagers/appDraftsManager.ts @@ -19,9 +19,9 @@ import { MessageEntity, DraftMessage, MessagesSaveDraft } from "../../layer"; import apiManager from "../mtproto/mtprotoworker"; import { tsNow } from "../../helpers/date"; import { deepEqual } from "../../helpers/object"; -import appStateManager from "./appStateManager"; import { isObject } from "../mtproto/bin_utils"; import { MOUNT_CLASS_TO } from "../../config/debug"; +import sessionStorage from "../sessionStorage"; export type MyDraftMessage = DraftMessage.draftMessage; @@ -30,8 +30,8 @@ export class AppDraftsManager { private getAllDraftPromise: Promise = null; constructor() { - appStateManager.getState().then(state => { - this.drafts = state.drafts; + sessionStorage.get('drafts').then(drafts => { + this.drafts = drafts || {}; }); rootScope.addMultipleEventsListeners({ @@ -96,7 +96,9 @@ export class AppDraftsManager { delete this.drafts[key]; } - appStateManager.pushToState('drafts', this.drafts); + sessionStorage.set({ + drafts: this.drafts + }); if(options.notify) { // console.warn(dT(), 'save draft', peerId, apiDraft, options) diff --git a/src/lib/appManagers/appStateManager.ts b/src/lib/appManagers/appStateManager.ts index e60f3b9d..e7689bd9 100644 --- a/src/lib/appManagers/appStateManager.ts +++ b/src/lib/appManagers/appStateManager.ts @@ -6,7 +6,7 @@ import type { Dialog } from './appMessagesManager'; import type { UserAuth } from '../mtproto/mtproto_config'; -import type { AppUsersManager } from './appUsersManager'; +import type { AppUsersManager, User } from './appUsersManager'; import type { AppChatsManager } from './appChatsManager'; import type { AuthState } from '../../types'; import type FiltersStorage from '../storages/filters'; @@ -17,9 +17,10 @@ import rootScope from '../rootScope'; import sessionStorage from '../sessionStorage'; import { logger } from '../logger'; import { copy, setDeepProperty, validateInitObject } from '../../helpers/object'; -import { getHeavyAnimationPromise } from '../../hooks/useHeavyAnimationCheck'; import App from '../../config/app'; import DEBUG, { MOUNT_CLASS_TO } from '../../config/debug'; +import AppStorage from '../storage'; +import { Chat } from '../../layer'; const REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day const STATE_VERSION = App.version; @@ -37,12 +38,9 @@ export type Theme = { background: Background }; -export type State = Partial<{ - dialogs: Dialog[], +export type State = { allDialogsLoaded: DialogsStorage['allDialogsLoaded'], - chats: {[peerId: string]: ReturnType}, - users: {[peerId: string]: ReturnType}, - messages: any[], + pinnedOrders: DialogsStorage['pinnedOrders'], contactsList: number[], updates: Partial<{ seq: number, @@ -84,16 +82,12 @@ export type State = Partial<{ }, nightTheme?: boolean, // ! DEPRECATED }, - keepSigned: boolean, - drafts: AppDraftsManager['drafts'] -}>; + keepSigned: boolean +}; export const STATE_INIT: State = { - dialogs: [], allDialogsLoaded: {}, - chats: {}, - users: {}, - messages: [], + pinnedOrders: {}, contactsList: [], updates: {}, filters: {}, @@ -147,14 +141,13 @@ export const STATE_INIT: State = { sound: false } }, - keepSigned: true, - drafts: {} + keepSigned: true }; const ALL_KEYS = Object.keys(STATE_INIT) as any as Array; const REFRESH_KEYS = ['dialogs', 'allDialogsLoaded', 'messages', 'contactsList', 'stateCreatedTime', - 'updates', 'maxSeenMsgId', 'filters', 'topPeers'] as any as Array; + 'updates', 'maxSeenMsgId', 'filters', 'topPeers', 'pinnedOrders'] as any as Array; export class AppStateManager extends EventListenerBase<{ save: (state: State) => Promise, @@ -163,8 +156,6 @@ export class AppStateManager extends EventListenerBase<{ }> { public static STATE_INIT = STATE_INIT; private loaded: Promise; - private loadPromises: Promise[] = []; - private loadAllPromise: Promise; private log = logger('STATE'/* , LogLevels.error */); private state: State; @@ -172,73 +163,147 @@ export class AppStateManager extends EventListenerBase<{ private neededPeers: Map> = new Map(); private singlePeerMap: Map = new Map(); + public storages = { + users: new AppStorage>({ + storeName: 'users' + }), + + chats: new AppStorage>({ + storeName: 'chats' + }), + + dialogs: new AppStorage>({ + storeName: 'dialogs' + }) + }; + + public storagesResults: {[key in keyof AppStateManager['storages']]: any[]} = {} as any; + constructor() { super(); this.loadSavedState(); } public loadSavedState(): Promise { - if(this.loadAllPromise) return this.loadAllPromise; - //console.time('load state'); + if(this.loaded) return this.loaded; + console.time('load state'); this.loaded = new Promise((resolve) => { - Promise.all(ALL_KEYS.concat('user_auth' as any).map(key => sessionStorage.get(key))).then((arr) => { - let state: State = {}; + const storagesKeys = Object.keys(this.storages) as Array; + const storagesPromises = storagesKeys.map(key => this.storages[key].getAll()); + + const promises = ALL_KEYS + .concat('user_auth' as any) + .map(key => sessionStorage.get(key)) + .concat(storagesPromises); + + Promise.all(promises).then((arr) => { + /* const self = this; + const skipHandleKeys = new Set(['isProxy', 'filters', 'drafts']); + const getHandler = (path?: string) => { + return { + get(target: any, key: any) { + if(key === 'isProxy') { + return true; + } + + const prop = target[key]; + + if(prop !== undefined && !skipHandleKeys.has(key) && !prop.isProxy && typeof(prop) === 'object') { + target[key] = new Proxy(prop, getHandler(path || key)); + return target[key]; + } + + return prop; + }, + set(target: any, key: any, value: any) { + console.log('Setting', target, `.${key} to equal`, value, path); + + target[key] = value; + + // @ts-ignore + self.pushToState(path || key, path ? self.state[path] : value, false); + + return true; + } + }; + }; */ + + let state: State = this.state = {} as any; // ! then can't store false values - ALL_KEYS.forEach((key, idx) => { - const value = arr[idx]; + for(let i = 0, length = ALL_KEYS.length; i < length; ++i) { + const key = ALL_KEYS[i]; + const value = arr[i]; if(value !== undefined) { // @ts-ignore state[key] = value; } else { - // @ts-ignore - state[key] = copy(STATE_INIT[key]); + this.pushToState(key, copy(STATE_INIT[key])); } - }); + } + + arr.splice(0, ALL_KEYS.length); + + // * Read auth + const auth: UserAuth = arr.shift() as any; + if(auth) { + // ! Warning ! DON'T delete this + state.authState = {_: 'authStateSignedIn'}; + rootScope.broadcast('user_auth', typeof(auth) !== 'number' ? (auth as any).id : auth); // * support old version + } + + // * Read storages + for(let i = 0, length = storagesKeys.length; i < length; ++i) { + this.storagesResults[storagesKeys[i]] = arr[i]; + } + + arr.splice(0, storagesKeys.length); const time = Date.now(); - /* if(state.version !== STATE_VERSION) { - state = copy(STATE_INIT); - } else */if((state.stateCreatedTime + REFRESH_EVERY) < time/* || true *//* && false */) { + if((state.stateCreatedTime + REFRESH_EVERY) < time) { if(DEBUG) { this.log('will refresh state', state.stateCreatedTime, time); } REFRESH_KEYS.forEach(key => { + this.pushToState(key, copy(STATE_INIT[key])); + // @ts-ignore - state[key] = copy(STATE_INIT[key]); + const s = this.storagesResults[key]; + if(s && s.length) { + s.length = 0; + } }); - - const users: typeof state['users'] = {}, chats: typeof state['chats'] = {}; - if(state.recentSearch?.length) { - state.recentSearch.forEach(peerId => { - if(peerId < 0) chats[peerId] = state.chats[peerId]; - else users[peerId] = state.users[peerId]; - }); - } - - state.users = users; - state.chats = chats; } + + //state = this.state = new Proxy(state, getHandler()); + // * support old version if(!state.settings.hasOwnProperty('themes') && state.settings.background) { const theme = STATE_INIT.settings.themes.find(t => t.name === STATE_INIT.settings.theme); if(theme) { - theme.background = copy(state.settings.background); + state.settings.themes.find(t => t.name === theme.name).background = copy(state.settings.background); + this.pushToState('settings', state.settings); } } + // * support old version if(!state.settings.hasOwnProperty('theme') && state.settings.hasOwnProperty('nightTheme')) { state.settings.theme = state.settings.nightTheme ? 'night' : 'day'; + this.pushToState('settings', state.settings); } - validateInitObject(STATE_INIT, state); + validateInitObject(STATE_INIT, state, (missingKey) => { + // @ts-ignore + this.pushToState(missingKey, state[missingKey]); + }); - this.state = state; - this.state.version = STATE_VERSION; + if(state.version !== STATE_VERSION) { + this.pushToState('version', STATE_VERSION); + } // ! probably there is better place for it - rootScope.settings = this.state.settings; + rootScope.settings = state.settings; if(DEBUG) { this.log('state res', state, copy(state)); @@ -246,29 +311,12 @@ export class AppStateManager extends EventListenerBase<{ //return resolve(); - const auth: UserAuth = arr[arr.length - 1] as any; - if(auth) { - // ! Warning ! DON'T delete this - this.state.authState = {_: 'authStateSignedIn'}; - rootScope.broadcast('user_auth', typeof(auth) !== 'number' ? (auth as any).id : auth); // * support old version - } - - //console.timeEnd('load state'); - resolve(this.state); + console.timeEnd('load state'); + resolve(state); }).catch(resolve); }); - return this.addLoadPromise(this.loaded); - } - - public addLoadPromise(promise: Promise) { - if(!this.loaded) { - return this.loadSavedState(); - } - - this.loadPromises.push(promise); - return this.loadAllPromise = Promise.all(this.loadPromises) - .then(() => this.state, () => this.state); + return this.loaded; } public getState() { @@ -284,20 +332,16 @@ export class AppStateManager extends EventListenerBase<{ this.pushToState(first, this.state[first]); } - public pushToState(key: T, value: State[T]) { - this.state[key] = value; + public pushToState(key: T, value: State[T], direct = true) { + if(direct) { + this.state[key] = value; + } sessionStorage.set({ [key]: value }); } - public setPeer(peerId: number, peer: any) { - const container = peerId > 0 ? this.state.users : this.state.chats; - if(container.hasOwnProperty(peerId)) return; - container[peerId] = peer; - } - public requestPeer(peerId: number, type: string, limit?: number) { let set = this.neededPeers.get(peerId); if(set && set.has(type)) { @@ -351,4 +395,4 @@ export class AppStateManager extends EventListenerBase<{ const appStateManager = new AppStateManager(); MOUNT_CLASS_TO.appStateManager = appStateManager; -export default appStateManager; \ No newline at end of file +export default appStateManager; diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index 57780173..bd5c7076 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -22,7 +22,6 @@ import serverTimeManager from "../mtproto/serverTimeManager"; import { RichTextProcessor } from "../richtextprocessor"; import rootScope from "../rootScope"; import searchIndexManager from "../searchIndexManager"; -import AppStorage from "../storage"; import apiUpdatesManager from "./apiUpdatesManager"; import appChatsManager from "./appChatsManager"; import appPeersManager from "./appPeersManager"; @@ -33,9 +32,7 @@ import appStateManager from "./appStateManager"; export type User = MTUser.user; export class AppUsersManager { - private storage = new AppStorage>({ - storeName: 'users' - }); + private storage = appStateManager.storages.users; private users: {[userId: number]: User} = {}; private usernames: {[username: string]: number} = {}; @@ -113,20 +110,16 @@ export class AppUsersManager { searchIndexManager.indexObject(userId, this.getUserSearchText(userId), this.contactsIndex); }); - let storageUsers: User[]; - const getStorageUsersPromise = this.storage.getAll().then(users => { - storageUsers = users as any; - }); - - appStateManager.addLoadPromise(getStorageUsersPromise).then((state) => { - if(storageUsers.length) { + appStateManager.getState().then((state) => { + const users = appStateManager.storagesResults.users; + if(users.length) { this.users = {}; - for(let i = 0, length = storageUsers.length; i < length; ++i) { - const user = storageUsers[i]; - this.users[user.id] = user; + for(let i = 0, length = users.length; i < length; ++i) { + const user = users[i]; + if(user) { + this.users[user.id] = user; + } } - } else if(state.users) { - this.users = state.users; } const contactsList = state.contactsList; diff --git a/src/lib/idb.ts b/src/lib/idb.ts index 1a2197f7..3977cb24 100644 --- a/src/lib/idb.ts +++ b/src/lib/idb.ts @@ -165,140 +165,39 @@ export default class IDBStorage { public delete(entryName: string | string[]): Promise { //return Promise.resolve(); - return this.openDatabase().then((db) => { - return new Promise((resolve, reject) => { - try { - //this.log('delete: `' + entryName + '`'); - const transaction = db.transaction([this.storeName], 'readwrite'); - const objectStore = transaction.objectStore(this.storeName); - - transaction.onerror = (e) => { - reject(transaction.error); - clearTimeout(timeout); - }; - - transaction.oncomplete = (e) => { - this.log('delete: transaction complete', entryName); - resolve(); - clearTimeout(timeout); - }; - - const timeout = setTimeout(() => { - this.log.error('delete: transaction not finished', entryName, transaction); - }, 10000); - - if(!Array.isArray(entryName)) { - entryName = [].concat(entryName); - } + if(!Array.isArray(entryName)) { + entryName = [].concat(entryName); + } - for(let i = 0, length = entryName.length; i < length; ++i) { - const request = objectStore.delete(entryName[i]); - request.onerror = (error) => { - reject(transaction.error); - clearTimeout(timeout); - }; - } - } catch(error) { - reject(error); - } - }); - }); + return this.getObjectStore('readwrite', (objectStore) => { + return (entryName as string[]).map((entryName) => objectStore.delete(entryName)); + }, 'delete: ' + entryName.join(', ')); } public deleteAll() { - return this.openDatabase().then((db) => { - //this.log('deleteAll'); - - try { - const transaction = db.transaction([this.storeName], 'readwrite'); - - const objectStore = transaction.objectStore(this.storeName); - var request = objectStore.clear(); - } catch(error) { - return Promise.reject(error); - } - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.log.error('deleteAll: request not finished', request); - }, 3000); - - request.onsuccess = (event) => { - resolve(); - clearTimeout(timeout); - }; - - request.onerror = (error) => { - reject(error); - clearTimeout(timeout); - }; - }); - }); + return this.getObjectStore('readwrite', (objectStore) => objectStore.clear(), 'deleteAll'); } public save(entryName: string | string[], value: any | any[]) { - return this.openDatabase().then((db) => { - //this.log('save:', entryName, value); - - 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); - } - }; - - return new Promise((resolve, reject) => { - try { - const transaction = db.transaction([this.storeName], 'readwrite'); - - transaction.onerror = (e) => { - handleError(transaction.error); - reject(transaction.error); - clearTimeout(timeout); - }; - - transaction.oncomplete = (e) => { - this.log('save: transaction complete:', entryName); - resolve(); - clearTimeout(timeout); - }; - - const timeout = setTimeout(() => { - this.log.error('save: transaction not finished', entryName, transaction); - }, 10000); - - /* transaction.addEventListener('abort', (e) => { - //handleError(); - this.log.error('IndexedDB: save transaction abort!', transaction.error); - }); */ - - const objectStore = transaction.objectStore(this.storeName); - - if(!Array.isArray(entryName)) { - entryName = [].concat(entryName); - value = [].concat(value); - } - - for(let i = 0, length = entryName.length; i < length; ++i) { - const request = objectStore.put(value[i], entryName[i]); - request.onerror = (error) => { - reject(transaction.error); - clearTimeout(timeout); - }; - } - } catch(error) { - handleError(error); - reject(error); - - /* this.storageIsAvailable = false; - throw error; */ - } - }); - }); + // 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)); + }, 'save: ' + entryName.join(', ')); } public saveFile(fileName: string, blob: Blob | Uint8Array) { @@ -374,97 +273,85 @@ export default class IDBStorage { return blob.size || blob.byteLength || blob.length; } */ - public get(entryName: string): Promise { + public get(entryName: string[]): Promise; + public get(entryName: string): Promise; + public get(entryName: string | string[]): Promise | Promise { //return Promise.reject(); - return this.openDatabase().then((db) => { - //this.log('get pre:', fileName); - - try { - const transaction = db.transaction([this.storeName], 'readonly'); - /* transaction.onabort = (e) => { - this.log.error('get transaction onabort?', e); - }; */ - const objectStore = transaction.objectStore(this.storeName); - var request = objectStore.get(entryName); - - //this.log.log('IDB get:', fileName, request); - } catch(err) { - this.log.error('get error:', err, entryName, request, request.error); - } - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.log.error('get request not finished!', entryName, request); - reject(); - }, 3000); - - request.onsuccess = function(event) { - const result = request.result; - if(result === undefined) { - reject('NO_ENTRY_FOUND'); - } /* else if(typeof result === 'string' && - result.substr(0, 5) === 'data:') { - resolve(dataUrlToBlob(result)); - } */else { - resolve(result); - } + if(!Array.isArray(entryName)) { + entryName = [].concat(entryName); + } - clearTimeout(timeout); - } - - request.onerror = () => { - clearTimeout(timeout); - reject(); - }; - }); - }); + return this.getObjectStore('readonly', (objectStore) => { + return (entryName as string[]).map((entryName) => objectStore.get(entryName)); + }, 'get: ' + entryName.join(', ')); } - public getAll(): Promise { - return this.openDatabase().then((db) => { - //this.log('getAll pre:', fileName); - - try { - const transaction = db.transaction([this.storeName], 'readonly'); - /* transaction.onabort = (e) => { - this.log.error('getAll transaction onabort?', e); - }; */ - const objectStore = transaction.objectStore(this.storeName); - var request = objectStore.getAll(); - - //this.log.log('IDB getAll:', fileName, request); - } catch(err) { - this.log.error('getAll error:', err, request, request.error); - } + private getObjectStore(mode: IDBTransactionMode, objectStore: (objectStore: IDBObjectStore) => IDBRequest | IDBRequest[], log: string) { + const perf = performance.now(); - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.log.error('getAll request not finished!', request); - reject(); - }, 3000); + this.log(log + ': start'); - request.onsuccess = function(event) { - const result = request.result; - if(result === undefined) { - reject('NO_ENTRY_FOUND'); - } /* else if(typeof result === 'string' && - result.substr(0, 5) === 'data:') { - resolve(dataUrlToBlob(result)); - } */else { - resolve(result); - } + return this.openDatabase().then((db) => { + return new Promise((resolve, reject) => { + const transaction = db.transaction([this.storeName], mode); + transaction.onerror = (e) => { clearTimeout(timeout); - } + reject(transaction.error); + }; - request.onerror = () => { + transaction.oncomplete = (e) => { clearTimeout(timeout); - reject(); + + 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(this.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(): Promise { + return this.getObjectStore('readonly', (objectStore) => objectStore.getAll(), 'getAll'); + } + /* public getAllKeys(): Promise> { console.time('getAllEntries'); return this.openDatabase().then((db) => { @@ -512,4 +399,4 @@ export default class IDBStorage { return Promise.resolve(fakeWriter); } */ -} \ No newline at end of file +} diff --git a/src/lib/mtproto/mtprotoworker.ts b/src/lib/mtproto/mtprotoworker.ts index 63b7eeb0..20b53641 100644 --- a/src/lib/mtproto/mtprotoworker.ts +++ b/src/lib/mtproto/mtprotoworker.ts @@ -312,6 +312,8 @@ export class ApiManagerProxy extends CryptoWorkerMethods { } private releasePending() { + //return; + if(this.postMessage) { this.debug && this.log.debug('releasing tasks, length:', this.pending.length); this.pending.forEach(pending => { diff --git a/src/lib/sessionStorage.ts b/src/lib/sessionStorage.ts index a24e96e2..c91b5f63 100644 --- a/src/lib/sessionStorage.ts +++ b/src/lib/sessionStorage.ts @@ -6,6 +6,7 @@ import type { ChatSavedPosition } from './appManagers/appImManager'; import type { State } from './appManagers/appStateManager'; +import type { AppDraftsManager } from './appManagers/appDraftsManager'; import { MOUNT_CLASS_TO } from '../config/debug'; import { LangPackDifference } from '../layer'; import AppStorage from './storage'; @@ -24,7 +25,8 @@ const sessionStorage = new AppStorage<{ chatPositions: { [peerId_threadId: string]: ChatSavedPosition }, - langPack: LangPackDifference + langPack: LangPackDifference, + drafts: AppDraftsManager['drafts'] } & State>({ storeName: 'session' }); diff --git a/src/lib/storage.ts b/src/lib/storage.ts index be310a7f..ac33c8a1 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -10,6 +10,7 @@ */ import { DatabaseStore, DatabaseStoreName } from "../config/database"; +import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise"; import { throttle } from "../helpers/schedulers"; import IDBStorage, { IDBOptions } from "./idb"; @@ -20,8 +21,13 @@ export default class AppStorage/* Storage ex //private cache: Partial<{[key: string]: Storage[typeof key]}> = {}; private cache: Partial = {}; private useStorage = true; + + private getPromises: Map> = new Map(); + private getThrottled: () => void; + private keysToSet: Set = new Set(); private saveThrottled: () => void; + private saveResolve: () => void; constructor(storageOptions: Omit & {stores?: DatabaseStore[], storeName: DatabaseStoreName}) { this.storage = new IDBStorage(storageOptions); @@ -29,24 +35,57 @@ export default class AppStorage/* Storage ex AppStorage.STORAGES.push(this); this.saveThrottled = throttle(async() => { - if(!this.keysToSet.size) { - return; + if(this.keysToSet.size) { + const keys = Array.from(this.keysToSet.values()) as string[]; + this.keysToSet.clear(); + + 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 */); + } } - const keys = Array.from(this.keysToSet.values()) as string[]; - this.keysToSet.clear(); - - 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 */); + if(this.saveResolve) { + this.saveResolve(); + this.saveResolve = undefined; } - }, 50, false); + }, 16, false); + + 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); + } + + 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); + this.getPromises.delete(key); + } + } + }); + }, 16, false); } public getCache() { @@ -65,19 +104,15 @@ export default class AppStorage/* Storage ex if(this.cache.hasOwnProperty(key)) { return this.getFromCache(key); } else if(this.useStorage) { - let value: any; - try { - value = await this.storage.get(key as string); - //console.log('[AS]: get result:', key, value); - //value = JSON.parse(value); - } catch(e) { - if(!['NO_ENTRY_FOUND', 'STORAGE_OFFLINE'].includes(e)) { - this.useStorage = false; - console.error('[AS]: get error:', e, key, value); - } - } + const r = this.getPromises.get(key); + if(r) return r; + + const p = deferredPromise(); + this.getPromises.set(key, p); - return this.cache[key] = value; + this.getThrottled(); + + return p; }/* else { throw 'something went wrong'; } */ @@ -87,7 +122,7 @@ export default class AppStorage/* Storage ex return this.storage.getAll(); } - public async set(obj: Partial, onlyLocal = false) { + public set(obj: Partial, onlyLocal = false) { //console.log('storageSetValue', obj, callback, arguments); for(const key in obj) { @@ -115,6 +150,10 @@ export default class AppStorage/* Storage ex } } } + + return new Promise((resolve) => { + this.saveResolve = resolve; + }); } public async delete(key: keyof Storage, saveLocal = false) { @@ -151,6 +190,8 @@ export default class AppStorage/* Storage ex if(!enabled) { storage.keysToSet.clear(); + storage.getPromises.forEach((deferred) => deferred.resolve()); + storage.getPromises.clear(); return storage.clear(); } else { return storage.set(storage.cache); diff --git a/src/lib/storages/dialogs.ts b/src/lib/storages/dialogs.ts index f73d8ff3..7fe9735f 100644 --- a/src/lib/storages/dialogs.ts +++ b/src/lib/storages/dialogs.ts @@ -23,14 +23,11 @@ import apiManager from "../mtproto/mtprotoworker"; import searchIndexManager from "../searchIndexManager"; import { forEachReverse, insertInDescendSortedArray } from "../../helpers/array"; import rootScope from "../rootScope"; -import AppStorage from "../storage"; import { safeReplaceObject } from "../../helpers/object"; import { AppStateManager } from "../appManagers/appStateManager"; export default class DialogsStorage { - private storage = new AppStorage>({ - storeName: 'dialogs' - }); + private storage: AppStateManager['storages']['dialogs']; private dialogs: {[peerId: string]: Dialog} = {}; public byFolders: {[folderId: number]: Dialog[]} = {}; @@ -64,7 +61,7 @@ export default class DialogsStorage { private apiUpdatesManager: ApiUpdatesManager, private serverTimeManager: ServerTimeManager ) { - this.dialogs = this.storage.getCache(); + this.storage = this.appStateManager.storages.dialogs; this.reset(); @@ -85,28 +82,33 @@ export default class DialogsStorage { updatePinnedDialogs: this.onUpdatePinnedDialogs, }); - let storageDialogs: Dialog[]; - const getStorageDialogsPromise = this.storage.getAll().then(dialogs => { - storageDialogs = dialogs as any; - - forEachReverse(storageDialogs, dialog => { - dialog.top_message = this.appMessagesManager.getServerMessageId(dialog.top_message); // * fix outgoing message to avoid copying dialog - - this.saveDialog(dialog); - - if(dialog.topMessage) { - this.appMessagesManager.saveMessages([dialog.topMessage]); - } + appStateManager.getState().then((state) => { + this.pinnedOrders = state.pinnedOrders || {}; + if(!this.pinnedOrders[0]) this.pinnedOrders[0] = []; + if(!this.pinnedOrders[1]) this.pinnedOrders[1] = []; + + const dialogs = appStateManager.storagesResults.dialogs; + if(dialogs.length) { + for(let i = 0, length = dialogs.length; i < length; ++i) { + const dialog = dialogs[i]; + if(dialog) { + dialog.top_message = this.appMessagesManager.getServerMessageId(dialog.top_message); // * fix outgoing message to avoid copying dialog + + if(dialog.topMessage) { + this.appMessagesManager.saveMessages([dialog.topMessage]); + } + + this.saveDialog(dialog); - // ! WARNING, убрать это когда нужно будет делать чтобы pending сообщения сохранялись - const message = this.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message); - if(message.deleted) { - this.appMessagesManager.reloadConversation(dialog.peerId); + // ! WARNING, убрать это когда нужно будет делать чтобы pending сообщения сохранялись + const message = this.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message); + if(message.deleted) { + this.appMessagesManager.reloadConversation(dialog.peerId); + } + } } - }); - }); + } - appStateManager.addLoadPromise(getStorageDialogsPromise).then((state) => { this.allDialogsLoaded = state.allDialogsLoaded || {}; }); } @@ -247,7 +249,11 @@ export default class DialogsStorage { const order = this.pinnedOrders[dialog.folder_id]; const foundIndex = order.indexOf(dialog.peerId); - const pinnedIndex = foundIndex === -1 ? order.push(dialog.peerId) - 1 : foundIndex; + let pinnedIndex = foundIndex; + if(foundIndex === -1) { + pinnedIndex = order.push(dialog.peerId) - 1; + this.appStateManager.pushToState('pinnedOrders', this.pinnedOrders); + } return this.generateDialogPinnedDateByIndex(pinnedIndex); } @@ -270,6 +276,38 @@ export default class DialogsStorage { return dialog; } + public setDialogToState(dialog: Dialog) { + const historyStorage = this.appMessagesManager.getHistoryStorage(dialog.peerId); + const history = [].concat(historyStorage.history.slice); + let incomingMessage: any; + for(let i = 0, length = history.length; i < length; ++i) { + const mid = history[i]; + const message = this.appMessagesManager.getMessageByPeer(dialog.peerId, mid); + if(!message.pFlags.is_outgoing) { + incomingMessage = message; + + if(message.fromId !== dialog.peerId) { + this.appStateManager.requestPeer(message.fromId, 'topMessage_' + dialog.peerId, 1); + } + + break; + } + } + + dialog.topMessage = incomingMessage; + + if(dialog.peerId < 0 && dialog.pts) { + const newPts = this.apiUpdatesManager.channelStates[-dialog.peerId].pts; + dialog.pts = newPts; + } + + this.storage.set({ + [dialog.peerId]: dialog + }); + + this.appStateManager.requestPeer(dialog.peerId, 'dialog'); + } + public pushDialog(dialog: Dialog, offsetDate?: number) { const dialogs = this.getFolder(dialog.folder_id); const pos = dialogs.findIndex(d => d.peerId === dialog.peerId); @@ -280,34 +318,7 @@ export default class DialogsStorage { //if(!this.dialogs[dialog.peerId]) { this.dialogs[dialog.peerId] = dialog; - const historyStorage = this.appMessagesManager.getHistoryStorage(dialog.peerId); - const history = [].concat(historyStorage.history.slice); - let incomingMessage: any; - for(const mid of history) { - const message = this.appMessagesManager.getMessageByPeer(dialog.peerId, mid); - if(!message.pFlags.is_outgoing) { - incomingMessage = message; - - if(message.fromId !== dialog.peerId) { - this.appStateManager.requestPeer(message.fromId, 'topMessage_' + dialog.peerId, 1); - } - - break; - } - } - - dialog.topMessage = incomingMessage; - - if(dialog.peerId < 0 && dialog.pts) { - const newPts = this.apiUpdatesManager.channelStates[-dialog.peerId].pts; - dialog.pts = newPts; - } - - this.storage.set({ - [dialog.peerId]: dialog - }); - - this.appStateManager.requestPeer(dialog.peerId, 'dialog'); + this.setDialogToState(dialog); //} if(offsetDate && @@ -591,6 +602,7 @@ export default class DialogsStorage { if(dialog.pFlags?.pinned) { delete dialog.pFlags.pinned; this.pinnedOrders[folder_id].findAndSplice(p => p === dialog.peerId); + this.appStateManager.pushToState('pinnedOrders', this.pinnedOrders); } dialog.folder_id = folder_id; @@ -622,6 +634,7 @@ export default class DialogsStorage { if(!update.pFlags.pinned) { delete dialog.pFlags.pinned; this.pinnedOrders[folderId].findAndSplice(p => p === dialog.peerId); + this.appStateManager.pushToState('pinnedOrders', this.pinnedOrders); } else { // means set dialog.pFlags.pinned = true; } diff --git a/src/lib/storages/filters.ts b/src/lib/storages/filters.ts index fa302087..48e84c45 100644 --- a/src/lib/storages/filters.ts +++ b/src/lib/storages/filters.ts @@ -42,11 +42,7 @@ export default class FiltersStorage { private rootScope: typeof _rootScope) { this.appStateManager.getState().then((state) => { - if(state.filters) { - for(const filterId in state.filters) { - this.saveDialogFilter(state.filters[filterId], false); - } - } + this.filters = state.filters; }); rootScope.addMultipleEventsListeners({