Browse Source

beta state fix

master
Eduard Kuzmenko 4 years ago
parent
commit
64efb48923
  1. 13
      src/helpers/object.ts
  2. 25
      src/lib/appManagers/appChatsManager.ts
  3. 4
      src/lib/appManagers/appDialogsManager.ts
  4. 10
      src/lib/appManagers/appDraftsManager.ts
  5. 198
      src/lib/appManagers/appStateManager.ts
  6. 25
      src/lib/appManagers/appUsersManager.ts
  7. 293
      src/lib/idb.ts
  8. 2
      src/lib/mtproto/mtprotoworker.ts
  9. 4
      src/lib/sessionStorage.ts
  10. 97
      src/lib/storage.ts
  11. 119
      src/lib/storages/dialogs.ts
  12. 6
      src/lib/storages/filters.ts

13
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; getDeepProperty(object, splitted.slice(0, -1).join('.'))[splitted.pop()] = value;
} }
export function validateInitObject(initObject: any, currentObject: any) { export function validateInitObject(initObject: any, currentObject: any, onReplace?: (key: string) => void, previousKey?: string) {
for(const i in initObject) { for(const key in initObject) {
if(typeof(currentObject[i]) !== typeof(initObject[i])) { if(typeof(currentObject[key]) !== typeof(initObject[key])) {
currentObject[i] = copy(initObject[i]); currentObject[key] = copy(initObject[key]);
} else if(isObject(initObject[i])) { onReplace && onReplace(previousKey || key);
validateInitObject(initObject[i], currentObject[i]); } else if(isObject(initObject[key])) {
validateInitObject(initObject[key], currentObject[key], onReplace, previousKey || key);
} }
} }
} }

25
src/lib/appManagers/appChatsManager.ts

@ -18,7 +18,6 @@ import apiManagerProxy from "../mtproto/mtprotoworker";
import apiManager from '../mtproto/mtprotoworker'; import apiManager from '../mtproto/mtprotoworker';
import { RichTextProcessor } from "../richtextprocessor"; import { RichTextProcessor } from "../richtextprocessor";
import rootScope from "../rootScope"; import rootScope from "../rootScope";
import AppStorage from "../storage";
import apiUpdatesManager from "./apiUpdatesManager"; import apiUpdatesManager from "./apiUpdatesManager";
import appMessagesManager from "./appMessagesManager"; import appMessagesManager from "./appMessagesManager";
import appPeersManager from "./appPeersManager"; 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 type UserTyping = Partial<{userId: number, action: SendMessageAction, timeout: number}>;
export class AppChatsManager { export class AppChatsManager {
private storage = new AppStorage<Record<number, Chat>>({ private storage = appStateManager.storages.chats;
storeName: 'chats'
});
private chats: {[id: number]: Chat.channel | Chat.chat | any} = {}; private chats: {[id: number]: Chat.channel | Chat.chat | any} = {};
//private usernames: any = {}; //private usernames: any = {};
@ -74,20 +71,16 @@ export class AppChatsManager {
updateChannelUserTyping: this.onUpdateUserTyping updateChannelUserTyping: this.onUpdateUserTyping
}); });
let storageChats: Chat[]; appStateManager.getState().then((state) => {
const getStorageChatsPromise = this.storage.getAll().then(chats => { const chats = appStateManager.storagesResults.chats;
storageChats = chats as any; if(chats.length) {
});
appStateManager.addLoadPromise(getStorageChatsPromise).then((state) => {
if(storageChats.length) {
this.chats = {}; this.chats = {};
for(let i = 0, length = storageChats.length; i < length; ++i) { for(let i = 0, length = chats.length; i < length; ++i) {
const user = storageChats[i]; const chat = chats[i];
this.chats[user.id] = user; if(chat) {
this.chats[chat.id] = chat;
}
} }
} else if(state.chats) {
this.chats = state.chats;
} }
appStateManager.addEventListener('peerNeeded', (peerId: number) => { appStateManager.addEventListener('peerNeeded', (peerId: number) => {

4
src/lib/appManagers/appDialogsManager.ts

@ -500,13 +500,15 @@ export class AppDialogsManager {
} }
}); });
if(state.dialogs?.length) { if(appStateManager.storagesResults.dialogs.length) {
appDraftsManager.getAllDrafts(); appDraftsManager.getAllDrafts();
appDraftsManager.addMissedDialogs(); appDraftsManager.addMissedDialogs();
} }
return this.loadDialogs(); return this.loadDialogs();
}).then(() => { }).then(() => {
return;
const isLoadedMain = appMessagesManager.dialogsStorage.isDialogsLoaded(0); const isLoadedMain = appMessagesManager.dialogsStorage.isDialogsLoaded(0);
const isLoadedArchive = appMessagesManager.dialogsStorage.isDialogsLoaded(1); const isLoadedArchive = appMessagesManager.dialogsStorage.isDialogsLoaded(1);
const wasLoaded = isLoadedMain || isLoadedArchive; const wasLoaded = isLoadedMain || isLoadedArchive;

10
src/lib/appManagers/appDraftsManager.ts

@ -19,9 +19,9 @@ import { MessageEntity, DraftMessage, MessagesSaveDraft } from "../../layer";
import apiManager from "../mtproto/mtprotoworker"; import apiManager from "../mtproto/mtprotoworker";
import { tsNow } from "../../helpers/date"; import { tsNow } from "../../helpers/date";
import { deepEqual } from "../../helpers/object"; import { deepEqual } from "../../helpers/object";
import appStateManager from "./appStateManager";
import { isObject } from "../mtproto/bin_utils"; import { isObject } from "../mtproto/bin_utils";
import { MOUNT_CLASS_TO } from "../../config/debug"; import { MOUNT_CLASS_TO } from "../../config/debug";
import sessionStorage from "../sessionStorage";
export type MyDraftMessage = DraftMessage.draftMessage; export type MyDraftMessage = DraftMessage.draftMessage;
@ -30,8 +30,8 @@ export class AppDraftsManager {
private getAllDraftPromise: Promise<void> = null; private getAllDraftPromise: Promise<void> = null;
constructor() { constructor() {
appStateManager.getState().then(state => { sessionStorage.get('drafts').then(drafts => {
this.drafts = state.drafts; this.drafts = drafts || {};
}); });
rootScope.addMultipleEventsListeners({ rootScope.addMultipleEventsListeners({
@ -96,7 +96,9 @@ export class AppDraftsManager {
delete this.drafts[key]; delete this.drafts[key];
} }
appStateManager.pushToState('drafts', this.drafts); sessionStorage.set({
drafts: this.drafts
});
if(options.notify) { if(options.notify) {
// console.warn(dT(), 'save draft', peerId, apiDraft, options) // console.warn(dT(), 'save draft', peerId, apiDraft, options)

198
src/lib/appManagers/appStateManager.ts

@ -6,7 +6,7 @@
import type { Dialog } from './appMessagesManager'; import type { Dialog } from './appMessagesManager';
import type { UserAuth } from '../mtproto/mtproto_config'; 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 { AppChatsManager } from './appChatsManager';
import type { AuthState } from '../../types'; import type { AuthState } from '../../types';
import type FiltersStorage from '../storages/filters'; import type FiltersStorage from '../storages/filters';
@ -17,9 +17,10 @@ import rootScope from '../rootScope';
import sessionStorage from '../sessionStorage'; import sessionStorage from '../sessionStorage';
import { logger } from '../logger'; import { logger } from '../logger';
import { copy, setDeepProperty, validateInitObject } from '../../helpers/object'; import { copy, setDeepProperty, validateInitObject } from '../../helpers/object';
import { getHeavyAnimationPromise } from '../../hooks/useHeavyAnimationCheck';
import App from '../../config/app'; import App from '../../config/app';
import DEBUG, { MOUNT_CLASS_TO } from '../../config/debug'; 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 REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day
const STATE_VERSION = App.version; const STATE_VERSION = App.version;
@ -37,12 +38,9 @@ export type Theme = {
background: Background background: Background
}; };
export type State = Partial<{ export type State = {
dialogs: Dialog[],
allDialogsLoaded: DialogsStorage['allDialogsLoaded'], allDialogsLoaded: DialogsStorage['allDialogsLoaded'],
chats: {[peerId: string]: ReturnType<AppChatsManager['getChat']>}, pinnedOrders: DialogsStorage['pinnedOrders'],
users: {[peerId: string]: ReturnType<AppUsersManager['getUser']>},
messages: any[],
contactsList: number[], contactsList: number[],
updates: Partial<{ updates: Partial<{
seq: number, seq: number,
@ -84,16 +82,12 @@ export type State = Partial<{
}, },
nightTheme?: boolean, // ! DEPRECATED nightTheme?: boolean, // ! DEPRECATED
}, },
keepSigned: boolean, keepSigned: boolean
drafts: AppDraftsManager['drafts'] };
}>;
export const STATE_INIT: State = { export const STATE_INIT: State = {
dialogs: [],
allDialogsLoaded: {}, allDialogsLoaded: {},
chats: {}, pinnedOrders: {},
users: {},
messages: [],
contactsList: [], contactsList: [],
updates: {}, updates: {},
filters: {}, filters: {},
@ -147,14 +141,13 @@ export const STATE_INIT: State = {
sound: false sound: false
} }
}, },
keepSigned: true, keepSigned: true
drafts: {}
}; };
const ALL_KEYS = Object.keys(STATE_INIT) as any as Array<keyof State>; const ALL_KEYS = Object.keys(STATE_INIT) as any as Array<keyof State>;
const REFRESH_KEYS = ['dialogs', 'allDialogsLoaded', 'messages', 'contactsList', 'stateCreatedTime', const REFRESH_KEYS = ['dialogs', 'allDialogsLoaded', 'messages', 'contactsList', 'stateCreatedTime',
'updates', 'maxSeenMsgId', 'filters', 'topPeers'] as any as Array<keyof State>; 'updates', 'maxSeenMsgId', 'filters', 'topPeers', 'pinnedOrders'] as any as Array<keyof State>;
export class AppStateManager extends EventListenerBase<{ export class AppStateManager extends EventListenerBase<{
save: (state: State) => Promise<void>, save: (state: State) => Promise<void>,
@ -163,8 +156,6 @@ export class AppStateManager extends EventListenerBase<{
}> { }> {
public static STATE_INIT = STATE_INIT; public static STATE_INIT = STATE_INIT;
private loaded: Promise<State>; private loaded: Promise<State>;
private loadPromises: Promise<any>[] = [];
private loadAllPromise: Promise<any>;
private log = logger('STATE'/* , LogLevels.error */); private log = logger('STATE'/* , LogLevels.error */);
private state: State; private state: State;
@ -172,73 +163,147 @@ export class AppStateManager extends EventListenerBase<{
private neededPeers: Map<number, Set<string>> = new Map(); private neededPeers: Map<number, Set<string>> = new Map();
private singlePeerMap: Map<string, number> = new Map(); private singlePeerMap: Map<string, number> = new Map();
public storages = {
users: new AppStorage<Record<number, User>>({
storeName: 'users'
}),
chats: new AppStorage<Record<number, Chat>>({
storeName: 'chats'
}),
dialogs: new AppStorage<Record<number, Dialog>>({
storeName: 'dialogs'
})
};
public storagesResults: {[key in keyof AppStateManager['storages']]: any[]} = {} as any;
constructor() { constructor() {
super(); super();
this.loadSavedState(); this.loadSavedState();
} }
public loadSavedState(): Promise<State> { public loadSavedState(): Promise<State> {
if(this.loadAllPromise) return this.loadAllPromise; if(this.loaded) return this.loaded;
//console.time('load state'); console.time('load state');
this.loaded = new Promise((resolve) => { this.loaded = new Promise((resolve) => {
Promise.all(ALL_KEYS.concat('user_auth' as any).map(key => sessionStorage.get(key))).then((arr) => { const storagesKeys = Object.keys(this.storages) as Array<keyof AppStateManager['storages']>;
let state: State = {}; 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 // ! then can't store false values
ALL_KEYS.forEach((key, idx) => { for(let i = 0, length = ALL_KEYS.length; i < length; ++i) {
const value = arr[idx]; const key = ALL_KEYS[i];
const value = arr[i];
if(value !== undefined) { if(value !== undefined) {
// @ts-ignore // @ts-ignore
state[key] = value; state[key] = value;
} else { } else {
// @ts-ignore this.pushToState(key, copy(STATE_INIT[key]));
state[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(); const time = Date.now();
/* if(state.version !== STATE_VERSION) { if((state.stateCreatedTime + REFRESH_EVERY) < time) {
state = copy(STATE_INIT);
} else */if((state.stateCreatedTime + REFRESH_EVERY) < time/* || true *//* && false */) {
if(DEBUG) { if(DEBUG) {
this.log('will refresh state', state.stateCreatedTime, time); this.log('will refresh state', state.stateCreatedTime, time);
} }
REFRESH_KEYS.forEach(key => { REFRESH_KEYS.forEach(key => {
this.pushToState(key, copy(STATE_INIT[key]));
// @ts-ignore // @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) { if(!state.settings.hasOwnProperty('themes') && state.settings.background) {
const theme = STATE_INIT.settings.themes.find(t => t.name === STATE_INIT.settings.theme); const theme = STATE_INIT.settings.themes.find(t => t.name === STATE_INIT.settings.theme);
if(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')) { if(!state.settings.hasOwnProperty('theme') && state.settings.hasOwnProperty('nightTheme')) {
state.settings.theme = state.settings.nightTheme ? 'night' : 'day'; 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; if(state.version !== STATE_VERSION) {
this.state.version = STATE_VERSION; this.pushToState('version', STATE_VERSION);
}
// ! probably there is better place for it // ! probably there is better place for it
rootScope.settings = this.state.settings; rootScope.settings = state.settings;
if(DEBUG) { if(DEBUG) {
this.log('state res', state, copy(state)); this.log('state res', state, copy(state));
@ -246,29 +311,12 @@ export class AppStateManager extends EventListenerBase<{
//return resolve(); //return resolve();
const auth: UserAuth = arr[arr.length - 1] as any; console.timeEnd('load state');
if(auth) { resolve(state);
// ! 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);
}).catch(resolve); }).catch(resolve);
}); });
return this.addLoadPromise(this.loaded); return this.loaded;
}
public addLoadPromise(promise: Promise<any>) {
if(!this.loaded) {
return this.loadSavedState();
}
this.loadPromises.push(promise);
return this.loadAllPromise = Promise.all(this.loadPromises)
.then(() => this.state, () => this.state);
} }
public getState() { public getState() {
@ -284,20 +332,16 @@ export class AppStateManager extends EventListenerBase<{
this.pushToState(first, this.state[first]); this.pushToState(first, this.state[first]);
} }
public pushToState<T extends keyof State>(key: T, value: State[T]) { public pushToState<T extends keyof State>(key: T, value: State[T], direct = true) {
this.state[key] = value; if(direct) {
this.state[key] = value;
}
sessionStorage.set({ sessionStorage.set({
[key]: value [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) { public requestPeer(peerId: number, type: string, limit?: number) {
let set = this.neededPeers.get(peerId); let set = this.neededPeers.get(peerId);
if(set && set.has(type)) { if(set && set.has(type)) {
@ -351,4 +395,4 @@ export class AppStateManager extends EventListenerBase<{
const appStateManager = new AppStateManager(); const appStateManager = new AppStateManager();
MOUNT_CLASS_TO.appStateManager = appStateManager; MOUNT_CLASS_TO.appStateManager = appStateManager;
export default appStateManager; export default appStateManager;

25
src/lib/appManagers/appUsersManager.ts

@ -22,7 +22,6 @@ import serverTimeManager from "../mtproto/serverTimeManager";
import { RichTextProcessor } from "../richtextprocessor"; import { RichTextProcessor } from "../richtextprocessor";
import rootScope from "../rootScope"; import rootScope from "../rootScope";
import searchIndexManager from "../searchIndexManager"; import searchIndexManager from "../searchIndexManager";
import AppStorage from "../storage";
import apiUpdatesManager from "./apiUpdatesManager"; import apiUpdatesManager from "./apiUpdatesManager";
import appChatsManager from "./appChatsManager"; import appChatsManager from "./appChatsManager";
import appPeersManager from "./appPeersManager"; import appPeersManager from "./appPeersManager";
@ -33,9 +32,7 @@ import appStateManager from "./appStateManager";
export type User = MTUser.user; export type User = MTUser.user;
export class AppUsersManager { export class AppUsersManager {
private storage = new AppStorage<Record<number, User>>({ private storage = appStateManager.storages.users;
storeName: 'users'
});
private users: {[userId: number]: User} = {}; private users: {[userId: number]: User} = {};
private usernames: {[username: string]: number} = {}; private usernames: {[username: string]: number} = {};
@ -113,20 +110,16 @@ export class AppUsersManager {
searchIndexManager.indexObject(userId, this.getUserSearchText(userId), this.contactsIndex); searchIndexManager.indexObject(userId, this.getUserSearchText(userId), this.contactsIndex);
}); });
let storageUsers: User[]; appStateManager.getState().then((state) => {
const getStorageUsersPromise = this.storage.getAll().then(users => { const users = appStateManager.storagesResults.users;
storageUsers = users as any; if(users.length) {
});
appStateManager.addLoadPromise(getStorageUsersPromise).then((state) => {
if(storageUsers.length) {
this.users = {}; this.users = {};
for(let i = 0, length = storageUsers.length; i < length; ++i) { for(let i = 0, length = users.length; i < length; ++i) {
const user = storageUsers[i]; const user = users[i];
this.users[user.id] = user; if(user) {
this.users[user.id] = user;
}
} }
} else if(state.users) {
this.users = state.users;
} }
const contactsList = state.contactsList; const contactsList = state.contactsList;

293
src/lib/idb.ts

@ -165,140 +165,39 @@ export default class IDBStorage {
public delete(entryName: string | string[]): Promise<void> { public delete(entryName: string | string[]): Promise<void> {
//return Promise.resolve(); //return Promise.resolve();
return this.openDatabase().then((db) => { if(!Array.isArray(entryName)) {
return new Promise((resolve, reject) => { entryName = [].concat(entryName);
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);
}
for(let i = 0, length = entryName.length; i < length; ++i) { return this.getObjectStore('readwrite', (objectStore) => {
const request = objectStore.delete(entryName[i]); return (entryName as string[]).map((entryName) => objectStore.delete(entryName));
request.onerror = (error) => { }, 'delete: ' + entryName.join(', '));
reject(transaction.error);
clearTimeout(timeout);
};
}
} catch(error) {
reject(error);
}
});
});
} }
public deleteAll() { public deleteAll() {
return this.openDatabase().then((db) => { return this.getObjectStore('readwrite', (objectStore) => objectStore.clear(), 'deleteAll');
//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<void>((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);
};
});
});
} }
public save(entryName: string | string[], value: any | any[]) { public save(entryName: string | string[], value: any | any[]) {
return this.openDatabase().then((db) => { // const handleError = (error: Error) => {
//this.log('save:', entryName, value); // this.log.error('save: transaction error:', entryName, value, db, error, error && error.name);
// if((!error || error.name === 'InvalidStateError')/* && false */) {
const handleError = (error: Error) => { // setTimeout(() => {
this.log.error('save: transaction error:', entryName, value, db, error, error && error.name); // this.save(entryName, value);
if((!error || error.name === 'InvalidStateError')/* && false */) { // }, 2e3);
setTimeout(() => { // } else {
this.save(entryName, value); // //console.error('IndexedDB saveFile transaction error:', error, error && error.name);
}, 2e3); // }
} else { // };
//console.error('IndexedDB saveFile transaction error:', error, error && error.name);
} if(!Array.isArray(entryName)) {
}; entryName = [].concat(entryName);
value = [].concat(value);
return new Promise<void>((resolve, reject) => { }
try {
const transaction = db.transaction([this.storeName], 'readwrite'); return this.getObjectStore('readwrite', (objectStore) => {
return (entryName as string[]).map((entryName, idx) => objectStore.put(value[idx], entryName));
transaction.onerror = (e) => { }, 'save: ' + entryName.join(', '));
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; */
}
});
});
} }
public saveFile(fileName: string, blob: Blob | Uint8Array) { public saveFile(fileName: string, blob: Blob | Uint8Array) {
@ -374,97 +273,85 @@ export default class IDBStorage {
return blob.size || blob.byteLength || blob.length; 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): Promise<T>;
public get<T>(entryName: string | string[]): Promise<T> | Promise<T[]> {
//return Promise.reject(); //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) => { if(!Array.isArray(entryName)) {
const timeout = setTimeout(() => { entryName = [].concat(entryName);
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);
}
clearTimeout(timeout); return this.getObjectStore<T>('readonly', (objectStore) => {
} return (entryName as string[]).map((entryName) => objectStore.get(entryName));
}, 'get: ' + entryName.join(', '));
request.onerror = () => {
clearTimeout(timeout);
reject();
};
});
});
} }
public getAll<T>(): Promise<T[]> { private getObjectStore<T>(mode: IDBTransactionMode, objectStore: (objectStore: IDBObjectStore) => IDBRequest | IDBRequest[], log: string) {
return this.openDatabase().then((db) => { const perf = performance.now();
//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);
}
return new Promise((resolve, reject) => { this.log(log + ': start');
const timeout = setTimeout(() => {
this.log.error('getAll request not finished!', request);
reject();
}, 3000);
request.onsuccess = function(event) { return this.openDatabase().then((db) => {
const result = request.result; return new Promise<T>((resolve, reject) => {
if(result === undefined) { const transaction = db.transaction([this.storeName], mode);
reject('NO_ENTRY_FOUND');
} /* else if(typeof result === 'string' &&
result.substr(0, 5) === 'data:') {
resolve(dataUrlToBlob(result));
} */else {
resolve(result);
}
transaction.onerror = (e) => {
clearTimeout(timeout); clearTimeout(timeout);
} reject(transaction.error);
};
request.onerror = () => { transaction.oncomplete = (e) => {
clearTimeout(timeout); 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<T>(): Promise<T[]> {
return this.getObjectStore<T[]>('readonly', (objectStore) => objectStore.getAll(), 'getAll');
}
/* public getAllKeys(): Promise<Array<string>> { /* public getAllKeys(): Promise<Array<string>> {
console.time('getAllEntries'); console.time('getAllEntries');
return this.openDatabase().then((db) => { return this.openDatabase().then((db) => {
@ -512,4 +399,4 @@ export default class IDBStorage {
return Promise.resolve(fakeWriter); return Promise.resolve(fakeWriter);
} */ } */
} }

2
src/lib/mtproto/mtprotoworker.ts

@ -312,6 +312,8 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
} }
private releasePending() { private releasePending() {
//return;
if(this.postMessage) { if(this.postMessage) {
this.debug && this.log.debug('releasing tasks, length:', this.pending.length); this.debug && this.log.debug('releasing tasks, length:', this.pending.length);
this.pending.forEach(pending => { this.pending.forEach(pending => {

4
src/lib/sessionStorage.ts

@ -6,6 +6,7 @@
import type { ChatSavedPosition } from './appManagers/appImManager'; import type { ChatSavedPosition } from './appManagers/appImManager';
import type { State } from './appManagers/appStateManager'; import type { State } from './appManagers/appStateManager';
import type { AppDraftsManager } from './appManagers/appDraftsManager';
import { MOUNT_CLASS_TO } from '../config/debug'; import { MOUNT_CLASS_TO } from '../config/debug';
import { LangPackDifference } from '../layer'; import { LangPackDifference } from '../layer';
import AppStorage from './storage'; import AppStorage from './storage';
@ -24,7 +25,8 @@ const sessionStorage = new AppStorage<{
chatPositions: { chatPositions: {
[peerId_threadId: string]: ChatSavedPosition [peerId_threadId: string]: ChatSavedPosition
}, },
langPack: LangPackDifference langPack: LangPackDifference,
drafts: AppDraftsManager['drafts']
} & State>({ } & State>({
storeName: 'session' storeName: 'session'
}); });

97
src/lib/storage.ts

@ -10,6 +10,7 @@
*/ */
import { DatabaseStore, DatabaseStoreName } from "../config/database"; import { DatabaseStore, DatabaseStoreName } from "../config/database";
import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise";
import { throttle } from "../helpers/schedulers"; import { throttle } from "../helpers/schedulers";
import IDBStorage, { IDBOptions } from "./idb"; import IDBStorage, { IDBOptions } from "./idb";
@ -20,8 +21,13 @@ export default class AppStorage<Storage extends Record<string, any>/* Storage ex
//private cache: Partial<{[key: string]: Storage[typeof key]}> = {}; //private cache: Partial<{[key: string]: Storage[typeof key]}> = {};
private cache: Partial<Storage> = {}; private cache: Partial<Storage> = {};
private useStorage = true; private useStorage = true;
private getPromises: Map<keyof Storage, CancellablePromise<Storage[keyof Storage]>> = new Map();
private getThrottled: () => void;
private keysToSet: Set<keyof Storage> = new Set(); private keysToSet: Set<keyof Storage> = new Set();
private saveThrottled: () => void; private saveThrottled: () => void;
private saveResolve: () => void;
constructor(storageOptions: Omit<IDBOptions, 'storeName' | 'stores'> & {stores?: DatabaseStore[], storeName: DatabaseStoreName}) { constructor(storageOptions: Omit<IDBOptions, 'storeName' | 'stores'> & {stores?: DatabaseStore[], storeName: DatabaseStoreName}) {
this.storage = new IDBStorage(storageOptions); this.storage = new IDBStorage(storageOptions);
@ -29,24 +35,57 @@ export default class AppStorage<Storage extends Record<string, any>/* Storage ex
AppStorage.STORAGES.push(this); AppStorage.STORAGES.push(this);
this.saveThrottled = throttle(async() => { this.saveThrottled = throttle(async() => {
if(!this.keysToSet.size) { if(this.keysToSet.size) {
return; 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[]; if(this.saveResolve) {
this.keysToSet.clear(); this.saveResolve();
this.saveResolve = undefined;
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 */);
} }
}, 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() { public getCache() {
@ -65,19 +104,15 @@ export default class AppStorage<Storage extends Record<string, any>/* Storage ex
if(this.cache.hasOwnProperty(key)) { if(this.cache.hasOwnProperty(key)) {
return this.getFromCache(key); return this.getFromCache(key);
} else if(this.useStorage) { } else if(this.useStorage) {
let value: any; const r = this.getPromises.get(key);
try { if(r) return r;
value = await this.storage.get(key as string);
//console.log('[AS]: get result:', key, value); const p = deferredPromise<Storage[typeof key]>();
//value = JSON.parse(value); this.getPromises.set(key, p);
} catch(e) {
if(!['NO_ENTRY_FOUND', 'STORAGE_OFFLINE'].includes(e)) {
this.useStorage = false;
console.error('[AS]: get error:', e, key, value);
}
}
return this.cache[key] = value; this.getThrottled();
return p;
}/* else { }/* else {
throw 'something went wrong'; throw 'something went wrong';
} */ } */
@ -87,7 +122,7 @@ export default class AppStorage<Storage extends Record<string, any>/* Storage ex
return this.storage.getAll(); return this.storage.getAll();
} }
public async set(obj: Partial<Storage>, onlyLocal = false) { public set(obj: Partial<Storage>, onlyLocal = false) {
//console.log('storageSetValue', obj, callback, arguments); //console.log('storageSetValue', obj, callback, arguments);
for(const key in obj) { for(const key in obj) {
@ -115,6 +150,10 @@ export default class AppStorage<Storage extends Record<string, any>/* Storage ex
} }
} }
} }
return new Promise<void>((resolve) => {
this.saveResolve = resolve;
});
} }
public async delete(key: keyof Storage, saveLocal = false) { public async delete(key: keyof Storage, saveLocal = false) {
@ -151,6 +190,8 @@ export default class AppStorage<Storage extends Record<string, any>/* Storage ex
if(!enabled) { if(!enabled) {
storage.keysToSet.clear(); storage.keysToSet.clear();
storage.getPromises.forEach((deferred) => deferred.resolve());
storage.getPromises.clear();
return storage.clear(); return storage.clear();
} else { } else {
return storage.set(storage.cache); return storage.set(storage.cache);

119
src/lib/storages/dialogs.ts

@ -23,14 +23,11 @@ import apiManager from "../mtproto/mtprotoworker";
import searchIndexManager from "../searchIndexManager"; import searchIndexManager from "../searchIndexManager";
import { forEachReverse, insertInDescendSortedArray } from "../../helpers/array"; import { forEachReverse, insertInDescendSortedArray } from "../../helpers/array";
import rootScope from "../rootScope"; import rootScope from "../rootScope";
import AppStorage from "../storage";
import { safeReplaceObject } from "../../helpers/object"; import { safeReplaceObject } from "../../helpers/object";
import { AppStateManager } from "../appManagers/appStateManager"; import { AppStateManager } from "../appManagers/appStateManager";
export default class DialogsStorage { export default class DialogsStorage {
private storage = new AppStorage<Record<number, Dialog>>({ private storage: AppStateManager['storages']['dialogs'];
storeName: 'dialogs'
});
private dialogs: {[peerId: string]: Dialog} = {}; private dialogs: {[peerId: string]: Dialog} = {};
public byFolders: {[folderId: number]: Dialog[]} = {}; public byFolders: {[folderId: number]: Dialog[]} = {};
@ -64,7 +61,7 @@ export default class DialogsStorage {
private apiUpdatesManager: ApiUpdatesManager, private apiUpdatesManager: ApiUpdatesManager,
private serverTimeManager: ServerTimeManager private serverTimeManager: ServerTimeManager
) { ) {
this.dialogs = this.storage.getCache(); this.storage = this.appStateManager.storages.dialogs;
this.reset(); this.reset();
@ -85,28 +82,33 @@ export default class DialogsStorage {
updatePinnedDialogs: this.onUpdatePinnedDialogs, updatePinnedDialogs: this.onUpdatePinnedDialogs,
}); });
let storageDialogs: Dialog[]; appStateManager.getState().then((state) => {
const getStorageDialogsPromise = this.storage.getAll().then(dialogs => { this.pinnedOrders = state.pinnedOrders || {};
storageDialogs = dialogs as any; if(!this.pinnedOrders[0]) this.pinnedOrders[0] = [];
if(!this.pinnedOrders[1]) this.pinnedOrders[1] = [];
forEachReverse(storageDialogs, dialog => {
dialog.top_message = this.appMessagesManager.getServerMessageId(dialog.top_message); // * fix outgoing message to avoid copying dialog const dialogs = appStateManager.storagesResults.dialogs;
if(dialogs.length) {
this.saveDialog(dialog); for(let i = 0, length = dialogs.length; i < length; ++i) {
const dialog = dialogs[i];
if(dialog.topMessage) { if(dialog) {
this.appMessagesManager.saveMessages([dialog.topMessage]); 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 сообщения сохранялись // ! WARNING, убрать это когда нужно будет делать чтобы pending сообщения сохранялись
const message = this.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message); const message = this.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message);
if(message.deleted) { if(message.deleted) {
this.appMessagesManager.reloadConversation(dialog.peerId); this.appMessagesManager.reloadConversation(dialog.peerId);
}
}
} }
}); }
});
appStateManager.addLoadPromise(getStorageDialogsPromise).then((state) => {
this.allDialogsLoaded = state.allDialogsLoaded || {}; this.allDialogsLoaded = state.allDialogsLoaded || {};
}); });
} }
@ -247,7 +249,11 @@ export default class DialogsStorage {
const order = this.pinnedOrders[dialog.folder_id]; const order = this.pinnedOrders[dialog.folder_id];
const foundIndex = order.indexOf(dialog.peerId); 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); return this.generateDialogPinnedDateByIndex(pinnedIndex);
} }
@ -270,6 +276,38 @@ export default class DialogsStorage {
return dialog; 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) { public pushDialog(dialog: Dialog, offsetDate?: number) {
const dialogs = this.getFolder(dialog.folder_id); const dialogs = this.getFolder(dialog.folder_id);
const pos = dialogs.findIndex(d => d.peerId === dialog.peerId); const pos = dialogs.findIndex(d => d.peerId === dialog.peerId);
@ -280,34 +318,7 @@ export default class DialogsStorage {
//if(!this.dialogs[dialog.peerId]) { //if(!this.dialogs[dialog.peerId]) {
this.dialogs[dialog.peerId] = dialog; this.dialogs[dialog.peerId] = dialog;
const historyStorage = this.appMessagesManager.getHistoryStorage(dialog.peerId); this.setDialogToState(dialog);
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');
//} //}
if(offsetDate && if(offsetDate &&
@ -591,6 +602,7 @@ export default class DialogsStorage {
if(dialog.pFlags?.pinned) { if(dialog.pFlags?.pinned) {
delete dialog.pFlags.pinned; delete dialog.pFlags.pinned;
this.pinnedOrders[folder_id].findAndSplice(p => p === dialog.peerId); this.pinnedOrders[folder_id].findAndSplice(p => p === dialog.peerId);
this.appStateManager.pushToState('pinnedOrders', this.pinnedOrders);
} }
dialog.folder_id = folder_id; dialog.folder_id = folder_id;
@ -622,6 +634,7 @@ export default class DialogsStorage {
if(!update.pFlags.pinned) { if(!update.pFlags.pinned) {
delete dialog.pFlags.pinned; delete dialog.pFlags.pinned;
this.pinnedOrders[folderId].findAndSplice(p => p === dialog.peerId); this.pinnedOrders[folderId].findAndSplice(p => p === dialog.peerId);
this.appStateManager.pushToState('pinnedOrders', this.pinnedOrders);
} else { // means set } else { // means set
dialog.pFlags.pinned = true; dialog.pFlags.pinned = true;
} }

6
src/lib/storages/filters.ts

@ -42,11 +42,7 @@ export default class FiltersStorage {
private rootScope: typeof _rootScope) { private rootScope: typeof _rootScope) {
this.appStateManager.getState().then((state) => { this.appStateManager.getState().then((state) => {
if(state.filters) { this.filters = state.filters;
for(const filterId in state.filters) {
this.saveDialogFilter(state.filters[filterId], false);
}
}
}); });
rootScope.addMultipleEventsListeners({ rootScope.addMultipleEventsListeners({

Loading…
Cancel
Save