Browse Source

beta state fix

master
Eduard Kuzmenko 3 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) { @@ -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);
}
}
}

25
src/lib/appManagers/appChatsManager.ts

@ -18,7 +18,6 @@ import apiManagerProxy from "../mtproto/mtprotoworker"; @@ -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 @@ -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<Record<number, Chat>>({
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 { @@ -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) => {

4
src/lib/appManagers/appDialogsManager.ts

@ -500,13 +500,15 @@ export class AppDialogsManager { @@ -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;

10
src/lib/appManagers/appDraftsManager.ts

@ -19,9 +19,9 @@ import { MessageEntity, DraftMessage, MessagesSaveDraft } from "../../layer"; @@ -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 { @@ -30,8 +30,8 @@ export class AppDraftsManager {
private getAllDraftPromise: Promise<void> = 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 { @@ -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)

198
src/lib/appManagers/appStateManager.ts

@ -6,7 +6,7 @@ @@ -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'; @@ -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 = { @@ -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<AppChatsManager['getChat']>},
users: {[peerId: string]: ReturnType<AppUsersManager['getUser']>},
messages: any[],
pinnedOrders: DialogsStorage['pinnedOrders'],
contactsList: number[],
updates: Partial<{
seq: number,
@ -84,16 +82,12 @@ export type State = Partial<{ @@ -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 = { @@ -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<keyof State>;
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<{
save: (state: State) => Promise<void>,
@ -163,8 +156,6 @@ export class AppStateManager extends EventListenerBase<{ @@ -163,8 +156,6 @@ export class AppStateManager extends EventListenerBase<{
}> {
public static STATE_INIT = STATE_INIT;
private loaded: Promise<State>;
private loadPromises: Promise<any>[] = [];
private loadAllPromise: Promise<any>;
private log = logger('STATE'/* , LogLevels.error */);
private state: State;
@ -172,73 +163,147 @@ export class AppStateManager extends EventListenerBase<{ @@ -172,73 +163,147 @@ export class AppStateManager extends EventListenerBase<{
private neededPeers: Map<number, Set<string>> = 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() {
super();
this.loadSavedState();
}
public loadSavedState(): Promise<State> {
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<keyof AppStateManager['storages']>;
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<{ @@ -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<any>) {
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<{ @@ -284,20 +332,16 @@ export class AppStateManager extends EventListenerBase<{
this.pushToState(first, this.state[first]);
}
public pushToState<T extends keyof State>(key: T, value: State[T]) {
this.state[key] = value;
public pushToState<T extends keyof State>(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<{ @@ -351,4 +395,4 @@ export class AppStateManager extends EventListenerBase<{
const appStateManager = new 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"; @@ -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"; @@ -33,9 +32,7 @@ import appStateManager from "./appStateManager";
export type User = MTUser.user;
export class AppUsersManager {
private storage = new AppStorage<Record<number, User>>({
storeName: 'users'
});
private storage = appStateManager.storages.users;
private users: {[userId: number]: User} = {};
private usernames: {[username: string]: number} = {};
@ -113,20 +110,16 @@ export class AppUsersManager { @@ -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;

293
src/lib/idb.ts

@ -165,140 +165,39 @@ export default class IDBStorage { @@ -165,140 +165,39 @@ export default class IDBStorage {
public delete(entryName: string | string[]): Promise<void> {
//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<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);
};
});
});
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<void>((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 { @@ -374,97 +273,85 @@ export default class IDBStorage {
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 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<T>('readonly', (objectStore) => {
return (entryName as string[]).map((entryName) => objectStore.get(entryName));
}, 'get: ' + entryName.join(', '));
}
public getAll<T>(): Promise<T[]> {
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<T>(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<T>((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<T>(): Promise<T[]> {
return this.getObjectStore<T[]>('readonly', (objectStore) => objectStore.getAll(), 'getAll');
}
/* public getAllKeys(): Promise<Array<string>> {
console.time('getAllEntries');
return this.openDatabase().then((db) => {
@ -512,4 +399,4 @@ export default class IDBStorage { @@ -512,4 +399,4 @@ export default class IDBStorage {
return Promise.resolve(fakeWriter);
} */
}
}

2
src/lib/mtproto/mtprotoworker.ts

@ -312,6 +312,8 @@ export class ApiManagerProxy extends CryptoWorkerMethods { @@ -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 => {

4
src/lib/sessionStorage.ts

@ -6,6 +6,7 @@ @@ -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<{ @@ -24,7 +25,8 @@ const sessionStorage = new AppStorage<{
chatPositions: {
[peerId_threadId: string]: ChatSavedPosition
},
langPack: LangPackDifference
langPack: LangPackDifference,
drafts: AppDraftsManager['drafts']
} & State>({
storeName: 'session'
});

97
src/lib/storage.ts

@ -10,6 +10,7 @@ @@ -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 extends Record<string, any>/* Storage ex @@ -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<Storage> = {};
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 saveThrottled: () => void;
private saveResolve: () => void;
constructor(storageOptions: Omit<IDBOptions, 'storeName' | 'stores'> & {stores?: DatabaseStore[], storeName: DatabaseStoreName}) {
this.storage = new IDBStorage(storageOptions);
@ -29,24 +35,57 @@ export default class AppStorage<Storage extends Record<string, any>/* Storage ex @@ -29,24 +35,57 @@ export default class AppStorage<Storage extends Record<string, any>/* 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 extends Record<string, any>/* Storage ex @@ -65,19 +104,15 @@ export default class AppStorage<Storage extends Record<string, any>/* 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<Storage[typeof key]>();
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 extends Record<string, any>/* Storage ex @@ -87,7 +122,7 @@ export default class AppStorage<Storage extends Record<string, any>/* Storage ex
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);
for(const key in obj) {
@ -115,6 +150,10 @@ export default class AppStorage<Storage extends Record<string, any>/* Storage ex @@ -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) {
@ -151,6 +190,8 @@ export default class AppStorage<Storage extends Record<string, any>/* Storage ex @@ -151,6 +190,8 @@ export default class AppStorage<Storage extends Record<string, any>/* 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);

119
src/lib/storages/dialogs.ts

@ -23,14 +23,11 @@ import apiManager from "../mtproto/mtprotoworker"; @@ -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<Record<number, Dialog>>({
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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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;
}

6
src/lib/storages/filters.ts

@ -42,11 +42,7 @@ export default class FiltersStorage { @@ -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({

Loading…
Cancel
Save