Cache: Autodelete old entries (#6580)
This commit is contained in:
parent
bb56647584
commit
d67efa3f99
@ -2568,3 +2568,6 @@
|
||||
"AttachmentSendFile_other" = "Send {count} Files";
|
||||
"AttachmentDragAddItems" = "Add Items";
|
||||
"AttachmentCaptionPlaceholder" = "Add a caption...";
|
||||
"SettingsDataClearMediaCache" = "Clear Media Cache";
|
||||
"SettingsDataClearMediaCacheDescription" = "Deletes locally cached media for this account";
|
||||
"SettingsDataClearMediaDone" = "Media cache cleared";
|
||||
|
||||
@ -1,21 +1,23 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import { memo, useCallback } from '../../../lib/teact/teact';
|
||||
import { memo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { AccountSettings } from '../../../types';
|
||||
|
||||
import { AUTODOWNLOAD_FILESIZE_MB_LIMITS } from '../../../config';
|
||||
import { purgeClearableCache } from '../../../util/cacheApi';
|
||||
import { pick } from '../../../util/iteratees';
|
||||
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import RangeSlider from '../../ui/RangeSlider';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
onReset: () => void;
|
||||
onReset: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = Pick<AccountSettings, (
|
||||
@ -34,9 +36,8 @@ type StateProps = Pick<AccountSettings, (
|
||||
'autoLoadFileMaxSizeMb'
|
||||
)>;
|
||||
|
||||
const SettingsDataStorage: FC<OwnProps & StateProps> = ({
|
||||
const SettingsDataStorage = ({
|
||||
isActive,
|
||||
onReset,
|
||||
canAutoLoadPhotoFromContacts,
|
||||
canAutoLoadPhotoInPrivateChats,
|
||||
canAutoLoadPhotoInGroups,
|
||||
@ -50,8 +51,9 @@ const SettingsDataStorage: FC<OwnProps & StateProps> = ({
|
||||
canAutoLoadFileInGroups,
|
||||
canAutoLoadFileInChannels,
|
||||
autoLoadFileMaxSizeMb,
|
||||
}) => {
|
||||
const { setSettingOption } = getActions();
|
||||
onReset,
|
||||
}: OwnProps & StateProps) => {
|
||||
const { setSettingOption, showNotification } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
@ -60,16 +62,23 @@ const SettingsDataStorage: FC<OwnProps & StateProps> = ({
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
const renderFileSizeCallback = useCallback((value: number) => {
|
||||
const renderFileSizeCallback = useLastCallback((value: number) => {
|
||||
const size = AUTODOWNLOAD_FILESIZE_MB_LIMITS[value];
|
||||
return lang('AutodownloadSizeLimitUpTo', {
|
||||
limit: lang('MediaSizeMB', { size }, { pluralValue: size }),
|
||||
});
|
||||
}, [lang]);
|
||||
});
|
||||
|
||||
const handleFileSizeChange = useCallback((value: number) => {
|
||||
const handleFileSizeChange = useLastCallback((value: number) => {
|
||||
setSettingOption({ autoLoadFileMaxSizeMb: AUTODOWNLOAD_FILESIZE_MB_LIMITS[value] });
|
||||
}, [setSettingOption]);
|
||||
});
|
||||
|
||||
const handlePurge = useLastCallback(() => {
|
||||
purgeClearableCache();
|
||||
showNotification({
|
||||
message: { key: 'SettingsDataClearMediaDone' },
|
||||
});
|
||||
});
|
||||
|
||||
function renderContentSizeSlider() {
|
||||
const value = AUTODOWNLOAD_FILESIZE_MB_LIMITS.indexOf(autoLoadFileMaxSizeMb);
|
||||
@ -157,6 +166,20 @@ const SettingsDataStorage: FC<OwnProps & StateProps> = ({
|
||||
canAutoLoadFileInGroups,
|
||||
canAutoLoadFileInChannels,
|
||||
)}
|
||||
<div className="settings-item">
|
||||
<ListItem
|
||||
onClick={handlePurge}
|
||||
icon="delete"
|
||||
multiline
|
||||
>
|
||||
<span className="title">
|
||||
{lang('SettingsDataClearMediaCache')}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{lang('SettingsDataClearMediaCacheDescription')}
|
||||
</span>
|
||||
</ListItem>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -61,7 +61,7 @@ export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false;
|
||||
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
|
||||
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
||||
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
|
||||
export const LANG_CACHE_NAME = 'tt-lang-packs-v50';
|
||||
export const LANG_CACHE_NAME = 'tt-lang-packs-v51';
|
||||
export const ASSET_CACHE_NAME = 'tt-assets';
|
||||
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
|
||||
export const DATA_BROADCAST_CHANNEL_PREFIX = 'tt-global';
|
||||
|
||||
@ -624,6 +624,23 @@ export function getMessageMediaHash(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getAllMessageMediaHashes(
|
||||
message: MediaContainer,
|
||||
statefulMedia: StatefulMediaContent,
|
||||
) {
|
||||
const targets: SizeTarget[] = ['micro', 'pictogram', 'inline', 'preview', 'full', 'download'];
|
||||
const hashes = new Set<string>();
|
||||
|
||||
targets.forEach((target) => {
|
||||
const hash = getMessageMediaHash(message, statefulMedia, target);
|
||||
if (hash) {
|
||||
hashes.add(hash);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(hashes);
|
||||
}
|
||||
|
||||
export function canReplaceMessageMedia(
|
||||
message: MediaContainer, attachment: ApiAttachment,
|
||||
) {
|
||||
|
||||
@ -26,7 +26,10 @@ import {
|
||||
areSortedArraysEqual, excludeSortedArray, omit, omitUndefined, pick, pickTruthy, unique,
|
||||
} from '../../util/iteratees';
|
||||
import { isLocalMessageId, type MessageKey } from '../../util/keys/messageKey';
|
||||
import { unload } from '../../util/mediaLoader';
|
||||
import {
|
||||
getAllMessageMediaHashes,
|
||||
getMessageStatefulContent,
|
||||
hasMessageTtl, isMediaLoadableInViewer, mergeIdRanges, orderHistoryIds, orderPinnedIds,
|
||||
} from '../helpers';
|
||||
import { getEmojiOnlyCountForMessage } from '../helpers/getEmojiOnlyCountForMessage';
|
||||
@ -380,6 +383,11 @@ export function deleteChatMessages<T extends GlobalState>(
|
||||
messageIds.forEach((messageId) => {
|
||||
const message = byId[messageId];
|
||||
if (!message) return;
|
||||
const statefulContent = getMessageStatefulContent(global, message);
|
||||
const hashes = getAllMessageMediaHashes(message, statefulContent);
|
||||
hashes.forEach((hash) => {
|
||||
unload(hash);
|
||||
});
|
||||
if (isMediaLoadableInViewer(message)) {
|
||||
mediaIdsToRemove.push(messageId);
|
||||
}
|
||||
|
||||
3
src/types/language.d.ts
vendored
3
src/types/language.d.ts
vendored
@ -1893,6 +1893,9 @@ export interface LangPair {
|
||||
'AttachmentMenuDisableSpoiler': undefined;
|
||||
'AttachmentDragAddItems': undefined;
|
||||
'AttachmentCaptionPlaceholder': undefined;
|
||||
'SettingsDataClearMediaCache': undefined;
|
||||
'SettingsDataClearMediaCacheDescription': undefined;
|
||||
'SettingsDataClearMediaDone': undefined;
|
||||
}
|
||||
|
||||
export interface LangPairWithVariables<V = LangVariable> {
|
||||
|
||||
10
src/util/browser/scheduler.ts
Normal file
10
src/util/browser/scheduler.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export async function yieldToMain() {
|
||||
const scheduler = (globalThis as any).scheduler;
|
||||
if (scheduler?.yield) {
|
||||
return scheduler.yield();
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
@ -1,8 +1,20 @@
|
||||
import { LANG_CACHE_NAME, MEDIA_CACHE_NAME, MEDIA_CACHE_NAME_AVATARS, MEDIA_PROGRESSIVE_CACHE_NAME } from '../config';
|
||||
import { yieldToMain } from './browser/scheduler';
|
||||
import { ACCOUNT_SLOT } from './multiaccount';
|
||||
|
||||
const cacheApi = self.caches;
|
||||
|
||||
const SUFFIX = ACCOUNT_SLOT ? `_${ACCOUNT_SLOT}` : '';
|
||||
const LAST_ACCESS_HEADER = 'X-Last-Access';
|
||||
const CACHE_TTL = 5 * 24 * 60 * 60 * 1000; // 5 days
|
||||
const ACCESS_THROTTLE = 24 * 60 * 60 * 1000; // 1 day
|
||||
const CLEANUP_INTERVAL = 1 * 60 * 60 * 1000; // 1 hour
|
||||
|
||||
const CLEARABLE_CACHE_NAMES = [MEDIA_CACHE_NAME, MEDIA_CACHE_NAME_AVATARS, MEDIA_PROGRESSIVE_CACHE_NAME];
|
||||
|
||||
cleanup(CLEARABLE_CACHE_NAMES);
|
||||
setInterval(() => {
|
||||
cleanup(CLEARABLE_CACHE_NAMES);
|
||||
}, CLEANUP_INTERVAL);
|
||||
|
||||
let isSupported: boolean | undefined;
|
||||
|
||||
@ -20,6 +32,13 @@ export enum Type {
|
||||
ArrayBuffer,
|
||||
}
|
||||
|
||||
function getCacheName(cacheName: string) {
|
||||
if (cacheName === LANG_CACHE_NAME) return cacheName;
|
||||
|
||||
const suffix = ACCOUNT_SLOT ? `_${ACCOUNT_SLOT}` : '';
|
||||
return `${cacheName}${suffix}`;
|
||||
}
|
||||
|
||||
export async function fetch(
|
||||
cacheName: string, key: string, type: Type, isHtmlAllowed = false,
|
||||
) {
|
||||
@ -30,12 +49,18 @@ export async function fetch(
|
||||
try {
|
||||
// To avoid the error "Request scheme 'webdocument' is unsupported"
|
||||
const request = new Request(key.replace(/:/g, '_'));
|
||||
const cache = await cacheApi.open(`${cacheName}${SUFFIX}`);
|
||||
const cache = await cacheApi.open(getCacheName(cacheName));
|
||||
const response = await cache.match(request);
|
||||
if (!response) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const lastAccess = Number(response.headers.get(LAST_ACCESS_HEADER));
|
||||
const now = Date.now();
|
||||
if (!lastAccess || now - lastAccess > ACCESS_THROTTLE) {
|
||||
updateAccessTime(cache, request, response);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
|
||||
switch (type) {
|
||||
@ -89,7 +114,8 @@ export async function save(cacheName: string, key: string, data: AnyLiteral | Bl
|
||||
// To avoid the error "Request scheme 'webdocument' is unsupported"
|
||||
const request = new Request(key.replace(/:/g, '_'));
|
||||
const response = new Response(cacheData);
|
||||
const cache = await cacheApi.open(`${cacheName}${SUFFIX}`);
|
||||
response.headers.set(LAST_ACCESS_HEADER, Date.now().toString());
|
||||
const cache = await cacheApi.open(getCacheName(cacheName));
|
||||
await cache.put(request, response);
|
||||
|
||||
return true;
|
||||
@ -106,7 +132,7 @@ export async function remove(cacheName: string, key: string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cache = await cacheApi.open(`${cacheName}${SUFFIX}`);
|
||||
const cache = await cacheApi.open(getCacheName(cacheName));
|
||||
return await cache.delete(key);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -121,10 +147,56 @@ export async function clear(cacheName: string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await cacheApi.delete(`${cacheName}${SUFFIX}`);
|
||||
return await cacheApi.delete(getCacheName(cacheName));
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanup(cacheNames: string[]) {
|
||||
if (!cacheApi) return;
|
||||
|
||||
try {
|
||||
for (const cacheName of cacheNames) {
|
||||
const cache = await cacheApi.open(getCacheName(cacheName));
|
||||
const keys = await cache.keys();
|
||||
const now = Date.now();
|
||||
|
||||
for (const request of keys) {
|
||||
await yieldToMain();
|
||||
const response = await cache.match(request);
|
||||
if (!response) continue;
|
||||
|
||||
const lastAccess = Number(response.headers.get(LAST_ACCESS_HEADER));
|
||||
if (lastAccess && now - lastAccess > CACHE_TTL) {
|
||||
await cache.delete(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
export function purgeClearableCache() {
|
||||
CLEARABLE_CACHE_NAMES.forEach((cacheName) => clear(cacheName));
|
||||
}
|
||||
|
||||
async function updateAccessTime(cache: Cache, request: Request, response: Response) {
|
||||
try {
|
||||
const headers = new Headers(response.headers);
|
||||
headers.set(LAST_ACCESS_HEADER, Date.now().toString());
|
||||
const newResponse = new Response(response.clone().body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
await cache.put(request, newResponse);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,6 +182,14 @@ async function fetchFromCacheOrRemote(
|
||||
return prepared;
|
||||
}
|
||||
|
||||
export async function unload(url: string) {
|
||||
memoryCache.delete(url);
|
||||
if (!MEDIA_CACHE_DISABLED) {
|
||||
const cacheName = url.startsWith('avatar') ? MEDIA_CACHE_NAME_AVATARS : MEDIA_CACHE_NAME;
|
||||
await cacheApi.remove(cacheName, url);
|
||||
}
|
||||
}
|
||||
|
||||
function makeOnProgress(url: string) {
|
||||
const onProgress: ApiOnProgress = (progress: number) => {
|
||||
progressCallbacks.get(url)?.forEach((callback) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user