Migrate session from IndexedDB to LocalStorage

This commit is contained in:
Eduard Kuzmenko 2021-06-15 04:19:58 +03:00
parent 38732e90cd
commit acf7f49dec
10 changed files with 321 additions and 85 deletions

View File

@ -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
}, {

View File

@ -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

View File

@ -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);

206
src/lib/localStorage.ts Normal file
View File

@ -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 <igor.beatle@gmail.com>
* 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<Storage extends Record<string, any>> {
private prefix = '';
private cache: Partial<Storage> = {};
private useStorage = true;
constructor(private preserveKeys: (keyof Storage)[]) {
if(Modes.test) {
this.prefix = 't_';
}
}
public get<T extends keyof Storage>(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<Storage>, 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<Storage> = {};
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<Storage extends Record<string, any>> {
private static STORAGES: LocalStorageController<any>[] = [];
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<Storage>;
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<T>(type: LocalStorageProxyTask['payload']['type'], ...args: LocalStorageProxyTask['payload']['args']) {
return new Promise<T>((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<T extends keyof Storage>(key: T, useCache?: boolean) {
return this.proxy<Storage[T]>('get', key, useCache);
}
public set(obj: Partial<Storage>, onlyLocal?: boolean) {
return this.proxy<void>('set', obj, onlyLocal);
}
public delete(key: keyof Storage, saveLocal?: boolean) {
return this.proxy<void>('delete', key, saveLocal);
}
public clear(preserveKeys?: (keyof Storage)[]) {
return this.proxy<void>('clear', preserveKeys);
}
public toggleStorage(enabled: boolean) {
return this.proxy<void>('toggleStorage', enabled);
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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<Storage extends Record<string, any>, T extends Database<any>/* Storage extends {[name: string]: any} *//* Storage extends Record<string, any> */> {
private static STORAGES: AppStorage<any, Database<any>>[] = [];
@ -57,6 +57,10 @@ export default class AppStorage<Storage extends Record<string, any>, T extends D
constructor(private db: T, storeName: typeof db['stores'][number]['name']) {
this.storage = new IDBStorage<T>(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<Storage extends Record<string, any>, 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<Storage extends Record<string, any>, 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<Storage extends Record<string, any>, 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<Storage extends Record<string, any>, T extends D
keys
}
} as LocalStorageProxyDeleteTask);
}
} */
await this.storage.delete(keys);
} catch(e) {
@ -275,19 +279,19 @@ export default class AppStorage<Storage extends Record<string, any>, 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);
}

View File

@ -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 => {