From f590963b1280fb5849bd8500d4398e332e80af46 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 6 Aug 2024 20:06:14 +0200 Subject: [PATCH] Global: Migrate cache from localStorage to IndexedDB (#4770) --- src/config.ts | 2 ++ src/global/cache.ts | 63 ++++++++++++++++++++++++++++++++------------ src/global/init.ts | 32 +++++----------------- src/index.tsx | 3 ++- src/util/init.ts | 37 ++++++++++++++++++++++++++ src/util/sessions.ts | 7 +++-- 6 files changed, 96 insertions(+), 48 deletions(-) create mode 100644 src/util/init.ts diff --git a/src/config.ts b/src/config.ts index efdb358c1..b4d4763f4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -42,6 +42,8 @@ export const GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT = 200; export const GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT = 10; export const GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT = 150; +export const IS_SCREEN_LOCKED_CACHE_KEY = 'tt-is-screen-locked'; + export const MEDIA_CACHE_DISABLED = false; export const MEDIA_CACHE_NAME = 'tt-media'; export const MEDIA_CACHE_NAME_AVATARS = 'tt-media-avatars'; diff --git a/src/global/cache.ts b/src/global/cache.ts index 3eb63bc04..bb2cd8354 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -19,8 +19,10 @@ import { GLOBAL_STATE_CACHE_DISABLED, GLOBAL_STATE_CACHE_KEY, GLOBAL_STATE_CACHE_USER_LIST_LIMIT, + IS_SCREEN_LOCKED_CACHE_KEY, SAVED_FOLDER_ID, } from '../config'; +import { MAIN_IDB_STORE } from '../util/browser/idb'; import { getOrderedIds } from '../util/folderManager'; import { compact, pick, pickTruthy, unique, @@ -50,21 +52,41 @@ const updateCacheThrottled = throttle(() => onIdle(() => updateCache()), UPDATE_ const updateCacheForced = () => updateCache(true); let isCaching = false; +let isRemovingCache = false; let unsubscribeFromBeforeUnload: NoneToVoidFunction | undefined; +export function cacheGlobal(global: GlobalState) { + return MAIN_IDB_STORE.set(GLOBAL_STATE_CACHE_KEY, global); +} + +export function loadCachedGlobal() { + return MAIN_IDB_STORE.get(GLOBAL_STATE_CACHE_KEY); +} + +export function removeGlobalFromCache() { + return MAIN_IDB_STORE.del(GLOBAL_STATE_CACHE_KEY); +} + +function cacheIsScreenLocked(global: GlobalState) { + if (global?.passcode?.isScreenLocked) localStorage.setItem(IS_SCREEN_LOCKED_CACHE_KEY, 'true'); +} + export function initCache() { if (GLOBAL_STATE_CACHE_DISABLED) { return; } const resetCache = () => { - localStorage.removeItem(GLOBAL_STATE_CACHE_KEY); + isRemovingCache = true; + removeGlobalFromCache().finally(() => { + localStorage.removeItem(IS_SCREEN_LOCKED_CACHE_KEY); + isRemovingCache = false; + if (!isCaching) { + return; + } - if (!isCaching) { - return; - } - - clearCaching(); + clearCaching(); + }); }; addActionHandler('saveSession', (): ActionReturnType => { @@ -79,12 +101,12 @@ export function initCache() { addActionHandler('reset', resetCache); } -export function loadCache(initialState: GlobalState): GlobalState | undefined { +export async function loadCache(initialState: GlobalState): Promise { if (GLOBAL_STATE_CACHE_DISABLED) { return undefined; } - const cache = readCache(initialState); + const cache = await readCache(initialState); if (cache.passcode.hasPasscode || hasStoredSession()) { setupCaching(); @@ -113,14 +135,17 @@ export function clearCaching() { } } -function readCache(initialState: GlobalState): GlobalState { +async function readCache(initialState: GlobalState): Promise { if (DEBUG) { // eslint-disable-next-line no-console console.time('global-state-cache-read'); } const json = localStorage.getItem(GLOBAL_STATE_CACHE_KEY); - const cached = json ? JSON.parse(json) as GlobalState : undefined; + const cachedFromLocalStorage = json ? JSON.parse(json) as GlobalState : undefined; + if (cachedFromLocalStorage) localStorage.removeItem(GLOBAL_STATE_CACHE_KEY); + + const cached = cachedFromLocalStorage || await loadCachedGlobal(); if (DEBUG) { // eslint-disable-next-line no-console @@ -231,7 +256,7 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { function updateCache(force?: boolean) { const global = getGlobal(); - if (!isCaching || global.isLoggingOut || (!force && isHeavyAnimating())) { + if (isRemovingCache || !isCaching || global.isLoggingOut || (!force && isHeavyAnimating())) { return; } @@ -248,16 +273,16 @@ export function forceUpdateCache(noEncrypt = false) { void encryptSession(undefined, serializedGlobal); } - const serializedGlobalClean = JSON.stringify(clearGlobalForLockScreen(global, false)); - localStorage.setItem(GLOBAL_STATE_CACHE_KEY, serializedGlobalClean); - + cacheIsScreenLocked(global); + cacheGlobal(clearGlobalForLockScreen(global, false)); return; } - localStorage.setItem(GLOBAL_STATE_CACHE_KEY, serializedGlobal); + cacheIsScreenLocked(global); + cacheGlobal(reduceGlobal(global)); } -export function serializeGlobal(global: T) { +function reduceGlobal(global: T) { const reducedGlobal: GlobalState = { ...INITIAL_GLOBAL_STATE, ...pick(global, [ @@ -314,7 +339,11 @@ export function serializeGlobal(global: T) { ]), }; - return JSON.stringify(reducedGlobal); + return reducedGlobal; +} + +export function serializeGlobal(global: T) { + return JSON.stringify(reduceGlobal(global)); } function reduceCustomEmojis(global: T): GlobalState['customEmojis'] { diff --git a/src/global/init.ts b/src/global/init.ts index 5a3782ee3..719b335ae 100644 --- a/src/global/init.ts +++ b/src/global/init.ts @@ -2,50 +2,30 @@ import './intervals'; import type { ActionReturnType, GlobalState } from './types'; -import { IS_MOCKED_CLIENT } from '../config'; import { isCacheApiSupported } from '../util/cacheApi'; import { getCurrentTabId, reestablishMasterToSelf } from '../util/establishMultitabRole'; +import { initGlobal } from '../util/init'; import { cloneDeep } from '../util/iteratees'; import { isLocalMessageId } from '../util/messageKey'; import { Bundles, loadBundle } from '../util/moduleLoader'; import { parseLocationHash } from '../util/routing'; -import { clearStoredSession } from '../util/sessions'; import { updatePeerColors } from '../util/theme'; import { IS_MULTITAB_SUPPORTED } from '../util/windowEnvironment'; import { initializeChatMediaSearchResults } from './reducers/localSearch'; import { updateTabState } from './reducers/tabs'; -import { initCache, loadCache } from './cache'; +import { initCache } from './cache'; import { addActionHandler, getGlobal, setGlobal, } from './index'; -import { INITIAL_GLOBAL_STATE, INITIAL_TAB_STATE } from './initialState'; -import { replaceTabThreadParam, replaceThreadParam, updatePasscodeSettings } from './reducers'; +import { INITIAL_TAB_STATE } from './initialState'; +import { replaceTabThreadParam, replaceThreadParam } from './reducers'; import { selectTabState, selectThreadParam } from './selectors'; initCache(); -addActionHandler('initShared', (prevGlobal, actions, payload): ActionReturnType => { +addActionHandler('initShared', async (prevGlobal, actions, payload): Promise => { const { force } = payload || {}; - if (!force && 'byTabId' in prevGlobal) return prevGlobal; - - const initial = cloneDeep(INITIAL_GLOBAL_STATE); - let global = loadCache(initial) || initial; - if (IS_MOCKED_CLIENT) global.authState = 'authorizationStateReady'; - - const { hasPasscode, isScreenLocked } = global.passcode; - if (hasPasscode && !isScreenLocked) { - global = updatePasscodeSettings(global, { - isScreenLocked: true, - }); - - clearStoredSession(); - } - - if (force) { - global.byTabId = prevGlobal.byTabId; - } - - return global; + await initGlobal(force, prevGlobal); }); addActionHandler('init', (global, actions, payload): ActionReturnType => { diff --git a/src/index.tsx b/src/index.tsx index fc351da27..eb3c0293e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,6 +15,7 @@ import { enableStrict, requestMutation } from './lib/fasterdom/fasterdom'; import { selectTabState } from './global/selectors'; import { betterView } from './util/betterView'; import { establishMultitabRole, subscribeToMasterChange } from './util/establishMultitabRole'; +import { initGlobal } from './util/init'; import { initLocalization } from './util/localization'; import { requestGlobal, subscribeToMultitabBroadcastChannel } from './util/multitab'; import { checkAndAssignPermanentWebVersion } from './util/permanentWebVersion'; @@ -57,7 +58,7 @@ async function init() { }); } - getActions().initShared(); + await initGlobal(); getActions().init(); getActions().updateShouldEnableDebugLog(); diff --git a/src/util/init.ts b/src/util/init.ts new file mode 100644 index 000000000..b6084ba94 --- /dev/null +++ b/src/util/init.ts @@ -0,0 +1,37 @@ +import type { GlobalState } from '../global/types'; + +import { IS_MOCKED_CLIENT } from '../config'; +import { loadCache } from '../global/cache'; +import { + getGlobal, setGlobal, +} from '../global/index'; +import { INITIAL_GLOBAL_STATE } from '../global/initialState'; +import { updatePasscodeSettings } from '../global/reducers'; +import { cloneDeep } from './iteratees'; +import { clearStoredSession } from './sessions'; + +export async function initGlobal(force: boolean = false, prevGlobal?: GlobalState) { + prevGlobal = prevGlobal || getGlobal(); + if (!force && 'byTabId' in prevGlobal) { + return; + } + + const initial = cloneDeep(INITIAL_GLOBAL_STATE); + let global = await loadCache(initial) || initial; + if (IS_MOCKED_CLIENT) global.authState = 'authorizationStateReady'; + + const { hasPasscode, isScreenLocked } = global.passcode; + if (hasPasscode && !isScreenLocked) { + global = updatePasscodeSettings(global, { + isScreenLocked: true, + }); + + clearStoredSession(); + } + + if (force) { + global.byTabId = prevGlobal.byTabId; + } + + setGlobal(global); +} diff --git a/src/util/sessions.ts b/src/util/sessions.ts index 564551fde..ef7c97320 100644 --- a/src/util/sessions.ts +++ b/src/util/sessions.ts @@ -1,7 +1,8 @@ import type { ApiSessionData } from '../api/types'; import { - DEBUG, GLOBAL_STATE_CACHE_KEY, SESSION_USER_KEY, + DEBUG, IS_SCREEN_LOCKED_CACHE_KEY, + SESSION_USER_KEY, } from '../config'; const DC_IDS = [1, 2, 3, 4, 5]; @@ -109,7 +110,5 @@ export function importTestSession() { } function checkSessionLocked() { - const stateFromCache = JSON.parse(localStorage.getItem(GLOBAL_STATE_CACHE_KEY) || '{}'); - - return Boolean(stateFromCache?.passcode?.isScreenLocked); + return localStorage.getItem(IS_SCREEN_LOCKED_CACHE_KEY) === 'true'; }