From f8069d3e8580b876bf06b9c309a4aeb62e5855c5 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Tue, 15 Jun 2021 04:19:58 +0300 Subject: [PATCH] Migrate session from IndexedDB to LocalStorage --- src/components/sidebarLeft/index.ts | 5 +- src/lib/appManagers/appStateManager.ts | 8 +- src/lib/cacheStorage.ts | 6 +- src/lib/localStorage.ts | 206 +++++++++++++++++++++++++ src/lib/mtproto/mtproto.worker.ts | 103 +++++++------ src/lib/mtproto/mtproto_config.ts | 4 +- src/lib/mtproto/mtprotoworker.ts | 34 ++-- src/lib/sessionStorage.ts | 10 +- src/lib/storage.ts | 28 ++-- src/pages/pageSignIn.ts | 2 + 10 files changed, 321 insertions(+), 85 deletions(-) create mode 100644 src/lib/localStorage.ts diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 4bba3eac..e1da3808 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -36,6 +36,7 @@ import PeerTitle from "../peerTitle"; import App from "../../config/app"; import ButtonMenuToggle from "../buttonMenuToggle"; import replaceContent from "../../helpers/dom/replaceContent"; +import sessionStorage from "../../lib/sessionStorage"; export const LEFT_COLUMN_ACTIVE_CLASSNAME = 'is-left-column-shown'; @@ -171,7 +172,9 @@ export class AppSidebarLeft extends SidebarSlider { icon: 'char z', text: 'ChatList.Menu.SwitchTo.Z', onClick: () => { - location.href = 'https://web.telegram.org/z/'; + sessionStorage.set({kz_version: 'z'}).then(() => { + location.href = 'https://web.telegram.org/z/'; + }); }, verify: () => App.isMainDomain }, { diff --git a/src/lib/appManagers/appStateManager.ts b/src/lib/appManagers/appStateManager.ts index ed2f005d..16c6d714 100644 --- a/src/lib/appManagers/appStateManager.ts +++ b/src/lib/appManagers/appStateManager.ts @@ -263,7 +263,7 @@ export class AppStateManager extends EventListenerBase<{ const values = await Promise.all(keys.map(key => stateStorage.get(key as any))); keys.push('user_auth'); - values.push(typeof(auth) === 'number' ? {dcID: values[0] || App.baseDcId, id: auth} : auth); + values.push(typeof(auth) === 'number' ? {dcID: values[0] || App.baseDcId, date: Date.now() / 1000 | 0, id: auth} as UserAuth : auth); let obj: any = {}; keys.forEach((key, idx) => { @@ -273,7 +273,7 @@ export class AppStateManager extends EventListenerBase<{ await sessionStorage.set(obj); } - if(!auth) { // try to read Webogram's session from localStorage + /* if(!auth) { // try to read Webogram's session from localStorage try { const keys = Object.keys(localStorage); for(let i = 0; i < keys.length; ++i) { @@ -295,12 +295,12 @@ export class AppStateManager extends EventListenerBase<{ } catch(err) { this.log.error('localStorage import error', err); } - } + } */ if(auth) { // ! Warning ! DON'T delete this state.authState = {_: 'authStateSignedIn'}; - rootScope.dispatchEvent('user_auth', typeof(auth) === 'number' ? {dcID: 0, id: auth} : auth); // * support old version + rootScope.dispatchEvent('user_auth', typeof(auth) === 'number' ? {dcID: 0, date: Date.now() / 1000 | 0, id: auth} : auth); // * support old version } // * Read storages diff --git a/src/lib/cacheStorage.ts b/src/lib/cacheStorage.ts index c6b66da2..804ea046 100644 --- a/src/lib/cacheStorage.ts +++ b/src/lib/cacheStorage.ts @@ -23,6 +23,10 @@ export default class CacheStorageController { if(Modes.test) { this.dbName += '_test'; } + + if(CacheStorageController.STORAGES.length) { + this.useStorage = CacheStorageController.STORAGES[0].useStorage; + } this.openDatabase(); CacheStorageController.STORAGES.push(this); @@ -131,7 +135,7 @@ export default class CacheStorageController { public getFileWriter(fileName: string, mimeType: string) { const fakeWriter = FileManager.getFakeFileWriter(mimeType, (blob) => { - return this.saveFile(fileName, blob); + return this.saveFile(fileName, blob).catch(() => blob); }); return Promise.resolve(fakeWriter); diff --git a/src/lib/localStorage.ts b/src/lib/localStorage.ts new file mode 100644 index 00000000..5e0a2f27 --- /dev/null +++ b/src/lib/localStorage.ts @@ -0,0 +1,206 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + * + * Originally from: + * https://github.com/zhukov/webogram + * Copyright (C) 2014 Igor Zhukov + * https://github.com/zhukov/webogram/blob/master/LICENSE + */ + +import Modes from '../config/modes'; +import { notifySomeone, isWorker } from '../helpers/context'; +import { WorkerTaskTemplate } from '../types'; +//import { stringify } from '../helpers/json'; + +class LocalStorage> { + private prefix = ''; + private cache: Partial = {}; + private useStorage = true; + + constructor(private preserveKeys: (keyof Storage)[]) { + if(Modes.test) { + this.prefix = 't_'; + } + } + + public get(key: T, useCache = true): Storage[T] { + if(this.cache.hasOwnProperty(key) && useCache) { + return this.cache[key]; + } else if(this.useStorage) { + let value: Storage[T]; + try { + value = localStorage.getItem(this.prefix + key as string) as any; + } catch(err) { + this.useStorage = false; + } + + if(value !== null) { + try { + value = JSON.parse(value); + } catch(err) { + //console.error(err); + } + } + + return value; + }/* else { + throw 'something went wrong'; + } */ + } + + public set(obj: Partial, onlyLocal = false) { + for(const key in obj) { + if(obj.hasOwnProperty(key)) { + const value = obj[key]; + this.cache[key] = value; + + if(this.useStorage && !onlyLocal) { + try { + const stringified = JSON.stringify(value); + localStorage.setItem(this.prefix + key, stringified); + } catch(err) { + this.useStorage = false; + } + } + } + } + } + + public delete(key: keyof Storage, saveLocal = false) { + // ! it is needed here + key = '' + key; + + if(!saveLocal) { + delete this.cache[key]; + } + + if(this.useStorage) { + localStorage.removeItem(this.prefix + key); + } + } + + public clear(preserveKeys: (keyof Storage)[] = this.preserveKeys) { + // if(this.useStorage) { + try { + let obj: Partial = {}; + if(preserveKeys) { + preserveKeys.forEach(key => { + const value = this.get(key); + if(value !== undefined) { + obj[key] = value; + } + }); + } + + localStorage.clear(); + + if(preserveKeys) { + this.set(obj); + } + } catch(err) { + + } + // } + } + + public toggleStorage(enabled: boolean) { + this.useStorage = enabled; + + if(!enabled) { + this.clear(); + } else { + return this.set(this.cache); + } + } +} + +export interface LocalStorageProxyTask extends WorkerTaskTemplate { + type: 'localStorageProxy', + payload: { + type: 'set' | 'get' | 'delete' | 'clear' | 'toggleStorage', + args: any[] + } +}; + +export interface LocalStorageProxyTaskResponse extends WorkerTaskTemplate { + type: 'localStorageProxy', + payload: any +}; + +export default class LocalStorageController> { + private static STORAGES: LocalStorageController[] = []; + private taskId = 0; + private tasks: {[taskID: number]: (result: any) => void} = {}; + //private log = (...args: any[]) => console.log('[SW LS]', ...args); + //private log = (...args: any[]) => {}; + + private storage: LocalStorage; + + constructor(private preserveKeys: (keyof Storage)[] = []) { + LocalStorageController.STORAGES.push(this); + + if(!isWorker) { + this.storage = new LocalStorage(preserveKeys); + } + } + + public finishTask(taskId: number, result: any) { + //this.log('finishTask:', taskID, result, Object.keys(this.tasks)); + + if(!this.tasks.hasOwnProperty(taskId)) { + //this.log('no such task:', taskID, result); + return; + } + + this.tasks[taskId](result); + delete this.tasks[taskId]; + } + + private proxy(type: LocalStorageProxyTask['payload']['type'], ...args: LocalStorageProxyTask['payload']['args']) { + return new Promise((resolve, reject) => { + if(isWorker) { + const taskId = this.taskId++; + + this.tasks[taskId] = resolve; + const task: LocalStorageProxyTask = { + type: 'localStorageProxy', + id: taskId, + payload: { + type, + args + } + }; + + notifySomeone(task); + } else { + args = Array.prototype.slice.call(args); + + // @ts-ignore + const result: any = this.storage[type].apply(this.storage, args as any); + resolve(result); + } + }); + } + + public get(key: T, useCache?: boolean) { + return this.proxy('get', key, useCache); + } + + public set(obj: Partial, onlyLocal?: boolean) { + return this.proxy('set', obj, onlyLocal); + } + + public delete(key: keyof Storage, saveLocal?: boolean) { + return this.proxy('delete', key, saveLocal); + } + + public clear(preserveKeys?: (keyof Storage)[]) { + return this.proxy('clear', preserveKeys); + } + + public toggleStorage(enabled: boolean) { + return this.proxy('toggleStorage', enabled); + } +} diff --git a/src/lib/mtproto/mtproto.worker.ts b/src/lib/mtproto/mtproto.worker.ts index 675131f2..4c3460e7 100644 --- a/src/lib/mtproto/mtproto.worker.ts +++ b/src/lib/mtproto/mtproto.worker.ts @@ -15,8 +15,11 @@ import type { ServiceWorkerTask, ServiceWorkerTaskResponse } from './mtproto.ser import { ctx } from '../../helpers/userAgent'; import { socketsProxied } from './dcConfigurator'; import { notifyAll } from '../../helpers/context'; -import AppStorage from '../storage'; +// import AppStorage from '../storage'; import CacheStorageController from '../cacheStorage'; +import sessionStorage from '../sessionStorage'; +import { LocalStorageProxyTask } from '../localStorage'; +import { WebpConvertTask } from '../webp/webpWorkerController'; let webpSupported = false; export const isWebpSupported = () => { @@ -31,53 +34,67 @@ networkerFactory.onConnectionStatusChange = (status) => { notifyAll({type: 'connectionStatusChange', payload: status}); }; +const taskListeners = { + convertWebp: (task: WebpConvertTask) => { + const {fileName, bytes} = task.payload; + const deferred = apiFileManager.webpConvertPromises[fileName]; + if(deferred) { + deferred.resolve(bytes); + delete apiFileManager.webpConvertPromises[fileName]; + } + }, + + requestFilePart: async(task: ServiceWorkerTask) => { + const responseTask: ServiceWorkerTaskResponse = { + type: task.type, + id: task.id + }; + + try { + const res = await apiFileManager.requestFilePart(...task.payload); + responseTask.payload = res; + } catch(err) { + responseTask.originalPayload = task.payload; + responseTask.error = err; + } + + notifyAll(responseTask); + }, + + webpSupport: (task: any) => { + webpSupported = task.payload; + }, + + socketProxy: (task: any) => { + const socketTask = task.payload; + const id = socketTask.id; + + const socketProxied = socketsProxied.get(id); + if(socketTask.type === 'message') { + socketProxied.dispatchEvent('message', socketTask.payload); + } else if(socketTask.type === 'open') { + socketProxied.dispatchEvent('open'); + } else if(socketTask.type === 'close') { + socketProxied.dispatchEvent('close'); + socketsProxied.delete(id); + } + }, + + localStorageProxy: (task: LocalStorageProxyTask) => { + sessionStorage.finishTask(task.id, task.payload); + } +}; + const onMessage = async(e: any) => { try { const task = e.data; const taskId = task.taskId; - if(task.type === 'convertWebp') { - const {fileName, bytes} = task.payload; - const deferred = apiFileManager.webpConvertPromises[fileName]; - if(deferred) { - deferred.resolve(bytes); - delete apiFileManager.webpConvertPromises[fileName]; - } - + // @ts-ignore + const f = taskListeners[task.type]; + if(f) { + f(task); return; - } else if((task as ServiceWorkerTask).type === 'requestFilePart') { - const task = e.data as ServiceWorkerTask; - const responseTask: ServiceWorkerTaskResponse = { - type: task.type, - id: task.id - }; - - try { - const res = await apiFileManager.requestFilePart(...task.payload); - responseTask.payload = res; - } catch(err) { - responseTask.originalPayload = task.payload; - responseTask.error = err; - } - - notifyAll(responseTask); - return; - } else if(task.type === 'webpSupport') { - webpSupported = task.payload; - return; - } else if(task.type === 'socketProxy') { - const socketTask = task.payload; - const id = socketTask.id; - - const socketProxied = socketsProxied.get(id); - if(socketTask.type === 'message') { - socketProxied.dispatchEvent('message', socketTask.payload); - } else if(socketTask.type === 'open') { - socketProxied.dispatchEvent('open'); - } else if(socketTask.type === 'close') { - socketProxied.dispatchEvent('close'); - socketsProxied.delete(id); - } } if(!task.task) { @@ -126,7 +143,7 @@ const onMessage = async(e: any) => { case 'toggleStorage': { const enabled = task.args[0]; - AppStorage.toggleStorage(enabled); + // AppStorage.toggleStorage(enabled); CacheStorageController.toggleStorage(enabled); break; } diff --git a/src/lib/mtproto/mtproto_config.ts b/src/lib/mtproto/mtproto_config.ts index 7ea3a3b2..20e7e831 100644 --- a/src/lib/mtproto/mtproto_config.ts +++ b/src/lib/mtproto/mtproto_config.ts @@ -5,8 +5,8 @@ */ /** - * Legacy Webogram's format, don't change dcID to camelCase. + * Legacy Webogram's format, don't change dcID to camelCase. date is timestamp */ -export type UserAuth = {dcID: number, id: number}; +export type UserAuth = {dcID: number | string, date: number, id: number}; export const REPLIES_PEER_ID = 1271266957; diff --git a/src/lib/mtproto/mtprotoworker.ts b/src/lib/mtproto/mtprotoworker.ts index 59e7ad7d..fae97f91 100644 --- a/src/lib/mtproto/mtprotoworker.ts +++ b/src/lib/mtproto/mtprotoworker.ts @@ -4,7 +4,8 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import type { LocalStorageProxyDeleteTask, LocalStorageProxySetTask } from '../storage'; +import type { LocalStorageProxyTask, LocalStorageProxyTaskResponse } from '../localStorage'; +//import type { LocalStorageProxyDeleteTask, LocalStorageProxySetTask } from '../storage'; import type { InvokeApiOptions } from '../../types'; import type { MethodDeclMap } from '../../layer'; import MTProtoWorker from 'worker-loader!./mtproto.worker'; @@ -22,6 +23,7 @@ import DEBUG, { MOUNT_CLASS_TO } from '../../config/debug'; import Socket from './transports/websocket'; import IDBStorage from '../idb'; import singleInstance from './singleInstance'; +import sessionStorage from '../sessionStorage'; type Task = { taskId: number, @@ -93,9 +95,10 @@ export class ApiManagerProxy extends CryptoWorkerMethods { this.registerServiceWorker(); this.addTaskListener('clear', () => { - const promise = IDBStorage.deleteDatabase(); - localStorage.clear(); // * clear legacy Webogram's localStorage - promise.finally(() => { + Promise.all([ + IDBStorage.deleteDatabase(), + sessionStorage.clear() + ]).finally(() => { location.reload(); }); }); @@ -164,19 +167,16 @@ export class ApiManagerProxy extends CryptoWorkerMethods { } }); - this.addTaskListener('localStorageProxy', (task: LocalStorageProxySetTask | LocalStorageProxyDeleteTask) => { + this.addTaskListener('localStorageProxy', (task: LocalStorageProxyTask) => { const storageTask = task.payload; - if(storageTask.type === 'set') { - for(let i = 0, length = storageTask.keys.length; i < length; ++i) { - if(storageTask.values[i] !== undefined) { - localStorage.setItem(storageTask.keys[i], JSON.stringify(storageTask.values[i])); - } - } - } else if(storageTask.type === 'delete') { - for(let i = 0, length = storageTask.keys.length; i < length; ++i) { - localStorage.removeItem(storageTask.keys[i]); - } - } + // @ts-ignore + sessionStorage[storageTask.type](...storageTask.args).then(res => { + this.postMessage({ + type: 'localStorageProxy', + id: task.id, + payload: res + } as LocalStorageProxyTaskResponse); + }); }); rootScope.addEventListener('language_change', (language) => { @@ -483,7 +483,7 @@ export class ApiManagerProxy extends CryptoWorkerMethods { public setUserAuth(userAuth: UserAuth | number) { if(typeof(userAuth) === 'number') { - userAuth = {dcID: 0, id: userAuth}; + userAuth = {dcID: 0, date: Date.now() / 1000 | 0, id: userAuth}; } rootScope.dispatchEvent('user_auth', userAuth); diff --git a/src/lib/sessionStorage.ts b/src/lib/sessionStorage.ts index fb32c27a..abf1ba0b 100644 --- a/src/lib/sessionStorage.ts +++ b/src/lib/sessionStorage.ts @@ -7,10 +7,9 @@ import type { AppInstance } from './mtproto/singleInstance'; import type { UserAuth } from './mtproto/mtproto_config'; import { MOUNT_CLASS_TO } from '../config/debug'; -import AppStorage from './storage'; -import DATABASE_SESSION from '../config/databases/session'; +import LocalStorageController from './localStorage'; -const sessionStorage = new AppStorage<{ +const sessionStorage = new LocalStorageController<{ dc: number, user_auth: UserAuth, dc1_auth_key: string, @@ -24,7 +23,8 @@ const sessionStorage = new AppStorage<{ dc4_server_salt: string, dc5_server_salt: string, server_time_offset: number, - xt_instance: AppInstance -}, typeof DATABASE_SESSION>(DATABASE_SESSION, 'session'); + xt_instance: AppInstance, + kz_version: 'k' | 'z' +}>(['kz_version']); MOUNT_CLASS_TO.appStorage = sessionStorage; export default sessionStorage; diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 3f1ae2ac..21d8cf79 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -10,15 +10,15 @@ */ import { Database } from "../config/databases"; -import DATABASE_SESSION from "../config/databases/session"; +//import DATABASE_SESSION from "../config/databases/session"; import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise"; import { throttle } from "../helpers/schedulers"; -import { WorkerTaskTemplate } from "../types"; +//import { WorkerTaskTemplate } from "../types"; import IDBStorage from "./idb"; function noop() {} -export interface LocalStorageProxySetTask extends WorkerTaskTemplate { +/* export interface LocalStorageProxySetTask extends WorkerTaskTemplate { type: 'localStorageProxy', payload: { type: 'set', @@ -33,7 +33,7 @@ export interface LocalStorageProxyDeleteTask extends WorkerTaskTemplate { type: 'delete', keys: string[] } -}; +}; */ export default class AppStorage, T extends Database/* Storage extends {[name: string]: any} *//* Storage extends Record */> { private static STORAGES: AppStorage>[] = []; @@ -57,6 +57,10 @@ export default class AppStorage, T extends D constructor(private db: T, storeName: typeof db['stores'][number]['name']) { this.storage = new IDBStorage(db, storeName); + if(AppStorage.STORAGES.length) { + this.useStorage = AppStorage.STORAGES[0].useStorage; + } + AppStorage.STORAGES.push(this); this.saveThrottled = throttle(async() => { @@ -74,7 +78,7 @@ export default class AppStorage, T extends D //await this.storage.save(key, new Response(value, {headers: {'Content-Type': 'application/json'}})); const values = keys.map(key => this.cache[key]); - if(db === DATABASE_SESSION && !('localStorage' in self)) { // * support legacy Webogram's localStorage + /* if(db === DATABASE_SESSION && !('localStorage' in self)) { // * support legacy Webogram's localStorage self.postMessage({ type: 'localStorageProxy', payload: { @@ -83,7 +87,7 @@ export default class AppStorage, T extends D values } } as LocalStorageProxySetTask); - } + } */ await this.storage.save(keys, values); //console.log('setItem: have set', key/* , value */); @@ -110,7 +114,7 @@ export default class AppStorage, T extends D set.clear(); try { - if(db === DATABASE_SESSION && !('localStorage' in self)) { // * support legacy Webogram's localStorage + /* if(db === DATABASE_SESSION && !('localStorage' in self)) { // * support legacy Webogram's localStorage self.postMessage({ type: 'localStorageProxy', payload: { @@ -118,7 +122,7 @@ export default class AppStorage, T extends D keys } } as LocalStorageProxyDeleteTask); - } + } */ await this.storage.delete(keys); } catch(e) { @@ -275,19 +279,19 @@ export default class AppStorage, T extends D storage.getPromises.forEach((deferred) => deferred.resolve()); storage.getPromises.clear(); - if(storage.db === DATABASE_SESSION && 'localStorage' in self) { // * support legacy Webogram's localStorage + /* if(storage.db === DATABASE_SESSION && 'localStorage' in self) { // * support legacy Webogram's localStorage localStorage.clear(); - } + } */ return storage.clear(); } else { - if(storage.db === DATABASE_SESSION && 'localStorage' in self) { // * support legacy Webogram's localStorage + /* if(storage.db === DATABASE_SESSION && 'localStorage' in self) { // * support legacy Webogram's localStorage for(const i in storage.cache) { if(storage.cache[i] !== undefined) { localStorage.setItem(i, JSON.stringify(storage.cache[i])); } } - } + } */ return storage.set(storage.cache); } diff --git a/src/pages/pageSignIn.ts b/src/pages/pageSignIn.ts index f5cf25e8..0fc864eb 100644 --- a/src/pages/pageSignIn.ts +++ b/src/pages/pageSignIn.ts @@ -33,6 +33,7 @@ import { cancelEvent } from "../helpers/dom/cancelEvent"; import { attachClickEvent } from "../helpers/dom/clickEvent"; import replaceContent from "../helpers/dom/replaceContent"; import toggleDisability from "../helpers/dom/toggleDisability"; +import sessionStorage from "../lib/sessionStorage"; type Country = _Country & { li?: HTMLLIElement[] @@ -332,6 +333,7 @@ let onFirstMount = () => { AppStorage.toggleStorage(keepSigned); CacheStorageController.toggleStorage(keepSigned); apiManager.toggleStorage(keepSigned); + sessionStorage.toggleStorage(keepSigned); }); appStateManager.getState().then(state => {