Cache: Autodelete old entries (#6580)
This commit is contained in:
parent
bb56647584
commit
d67efa3f99
@ -2568,3 +2568,6 @@
|
|||||||
"AttachmentSendFile_other" = "Send {count} Files";
|
"AttachmentSendFile_other" = "Send {count} Files";
|
||||||
"AttachmentDragAddItems" = "Add Items";
|
"AttachmentDragAddItems" = "Add Items";
|
||||||
"AttachmentCaptionPlaceholder" = "Add a caption...";
|
"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 } from '../../../lib/teact/teact';
|
||||||
import { memo, useCallback } from '../../../lib/teact/teact';
|
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { AccountSettings } from '../../../types';
|
import type { AccountSettings } from '../../../types';
|
||||||
|
|
||||||
import { AUTODOWNLOAD_FILESIZE_MB_LIMITS } from '../../../config';
|
import { AUTODOWNLOAD_FILESIZE_MB_LIMITS } from '../../../config';
|
||||||
|
import { purgeClearableCache } from '../../../util/cacheApi';
|
||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
|
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
import Checkbox from '../../ui/Checkbox';
|
import Checkbox from '../../ui/Checkbox';
|
||||||
|
import ListItem from '../../ui/ListItem';
|
||||||
import RangeSlider from '../../ui/RangeSlider';
|
import RangeSlider from '../../ui/RangeSlider';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
onReset: () => void;
|
onReset: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = Pick<AccountSettings, (
|
type StateProps = Pick<AccountSettings, (
|
||||||
@ -34,9 +36,8 @@ type StateProps = Pick<AccountSettings, (
|
|||||||
'autoLoadFileMaxSizeMb'
|
'autoLoadFileMaxSizeMb'
|
||||||
)>;
|
)>;
|
||||||
|
|
||||||
const SettingsDataStorage: FC<OwnProps & StateProps> = ({
|
const SettingsDataStorage = ({
|
||||||
isActive,
|
isActive,
|
||||||
onReset,
|
|
||||||
canAutoLoadPhotoFromContacts,
|
canAutoLoadPhotoFromContacts,
|
||||||
canAutoLoadPhotoInPrivateChats,
|
canAutoLoadPhotoInPrivateChats,
|
||||||
canAutoLoadPhotoInGroups,
|
canAutoLoadPhotoInGroups,
|
||||||
@ -50,8 +51,9 @@ const SettingsDataStorage: FC<OwnProps & StateProps> = ({
|
|||||||
canAutoLoadFileInGroups,
|
canAutoLoadFileInGroups,
|
||||||
canAutoLoadFileInChannels,
|
canAutoLoadFileInChannels,
|
||||||
autoLoadFileMaxSizeMb,
|
autoLoadFileMaxSizeMb,
|
||||||
}) => {
|
onReset,
|
||||||
const { setSettingOption } = getActions();
|
}: OwnProps & StateProps) => {
|
||||||
|
const { setSettingOption, showNotification } = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
@ -60,16 +62,23 @@ const SettingsDataStorage: FC<OwnProps & StateProps> = ({
|
|||||||
onBack: onReset,
|
onBack: onReset,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderFileSizeCallback = useCallback((value: number) => {
|
const renderFileSizeCallback = useLastCallback((value: number) => {
|
||||||
const size = AUTODOWNLOAD_FILESIZE_MB_LIMITS[value];
|
const size = AUTODOWNLOAD_FILESIZE_MB_LIMITS[value];
|
||||||
return lang('AutodownloadSizeLimitUpTo', {
|
return lang('AutodownloadSizeLimitUpTo', {
|
||||||
limit: lang('MediaSizeMB', { size }, { pluralValue: size }),
|
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({ autoLoadFileMaxSizeMb: AUTODOWNLOAD_FILESIZE_MB_LIMITS[value] });
|
||||||
}, [setSettingOption]);
|
});
|
||||||
|
|
||||||
|
const handlePurge = useLastCallback(() => {
|
||||||
|
purgeClearableCache();
|
||||||
|
showNotification({
|
||||||
|
message: { key: 'SettingsDataClearMediaDone' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function renderContentSizeSlider() {
|
function renderContentSizeSlider() {
|
||||||
const value = AUTODOWNLOAD_FILESIZE_MB_LIMITS.indexOf(autoLoadFileMaxSizeMb);
|
const value = AUTODOWNLOAD_FILESIZE_MB_LIMITS.indexOf(autoLoadFileMaxSizeMb);
|
||||||
@ -157,6 +166,20 @@ const SettingsDataStorage: FC<OwnProps & StateProps> = ({
|
|||||||
canAutoLoadFileInGroups,
|
canAutoLoadFileInGroups,
|
||||||
canAutoLoadFileInChannels,
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false;
|
|||||||
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
|
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
|
||||||
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
||||||
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
|
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 ASSET_CACHE_NAME = 'tt-assets';
|
||||||
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
|
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
|
||||||
export const DATA_BROADCAST_CHANNEL_PREFIX = 'tt-global';
|
export const DATA_BROADCAST_CHANNEL_PREFIX = 'tt-global';
|
||||||
|
|||||||
@ -624,6 +624,23 @@ export function getMessageMediaHash(
|
|||||||
return undefined;
|
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(
|
export function canReplaceMessageMedia(
|
||||||
message: MediaContainer, attachment: ApiAttachment,
|
message: MediaContainer, attachment: ApiAttachment,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -26,7 +26,10 @@ import {
|
|||||||
areSortedArraysEqual, excludeSortedArray, omit, omitUndefined, pick, pickTruthy, unique,
|
areSortedArraysEqual, excludeSortedArray, omit, omitUndefined, pick, pickTruthy, unique,
|
||||||
} from '../../util/iteratees';
|
} from '../../util/iteratees';
|
||||||
import { isLocalMessageId, type MessageKey } from '../../util/keys/messageKey';
|
import { isLocalMessageId, type MessageKey } from '../../util/keys/messageKey';
|
||||||
|
import { unload } from '../../util/mediaLoader';
|
||||||
import {
|
import {
|
||||||
|
getAllMessageMediaHashes,
|
||||||
|
getMessageStatefulContent,
|
||||||
hasMessageTtl, isMediaLoadableInViewer, mergeIdRanges, orderHistoryIds, orderPinnedIds,
|
hasMessageTtl, isMediaLoadableInViewer, mergeIdRanges, orderHistoryIds, orderPinnedIds,
|
||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
import { getEmojiOnlyCountForMessage } from '../helpers/getEmojiOnlyCountForMessage';
|
import { getEmojiOnlyCountForMessage } from '../helpers/getEmojiOnlyCountForMessage';
|
||||||
@ -380,6 +383,11 @@ export function deleteChatMessages<T extends GlobalState>(
|
|||||||
messageIds.forEach((messageId) => {
|
messageIds.forEach((messageId) => {
|
||||||
const message = byId[messageId];
|
const message = byId[messageId];
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
|
const statefulContent = getMessageStatefulContent(global, message);
|
||||||
|
const hashes = getAllMessageMediaHashes(message, statefulContent);
|
||||||
|
hashes.forEach((hash) => {
|
||||||
|
unload(hash);
|
||||||
|
});
|
||||||
if (isMediaLoadableInViewer(message)) {
|
if (isMediaLoadableInViewer(message)) {
|
||||||
mediaIdsToRemove.push(messageId);
|
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;
|
'AttachmentMenuDisableSpoiler': undefined;
|
||||||
'AttachmentDragAddItems': undefined;
|
'AttachmentDragAddItems': undefined;
|
||||||
'AttachmentCaptionPlaceholder': undefined;
|
'AttachmentCaptionPlaceholder': undefined;
|
||||||
|
'SettingsDataClearMediaCache': undefined;
|
||||||
|
'SettingsDataClearMediaCacheDescription': undefined;
|
||||||
|
'SettingsDataClearMediaDone': undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LangPairWithVariables<V = LangVariable> {
|
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';
|
import { ACCOUNT_SLOT } from './multiaccount';
|
||||||
|
|
||||||
const cacheApi = self.caches;
|
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;
|
let isSupported: boolean | undefined;
|
||||||
|
|
||||||
@ -20,6 +32,13 @@ export enum Type {
|
|||||||
ArrayBuffer,
|
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(
|
export async function fetch(
|
||||||
cacheName: string, key: string, type: Type, isHtmlAllowed = false,
|
cacheName: string, key: string, type: Type, isHtmlAllowed = false,
|
||||||
) {
|
) {
|
||||||
@ -30,12 +49,18 @@ export async function fetch(
|
|||||||
try {
|
try {
|
||||||
// To avoid the error "Request scheme 'webdocument' is unsupported"
|
// To avoid the error "Request scheme 'webdocument' is unsupported"
|
||||||
const request = new Request(key.replace(/:/g, '_'));
|
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);
|
const response = await cache.match(request);
|
||||||
if (!response) {
|
if (!response) {
|
||||||
return undefined;
|
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');
|
const contentType = response.headers.get('Content-Type');
|
||||||
|
|
||||||
switch (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"
|
// To avoid the error "Request scheme 'webdocument' is unsupported"
|
||||||
const request = new Request(key.replace(/:/g, '_'));
|
const request = new Request(key.replace(/:/g, '_'));
|
||||||
const response = new Response(cacheData);
|
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);
|
await cache.put(request, response);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -106,7 +132,7 @@ export async function remove(cacheName: string, key: string) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cache = await cacheApi.open(`${cacheName}${SUFFIX}`);
|
const cache = await cacheApi.open(getCacheName(cacheName));
|
||||||
return await cache.delete(key);
|
return await cache.delete(key);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@ -121,10 +147,56 @@ export async function clear(cacheName: string) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await cacheApi.delete(`${cacheName}${SUFFIX}`);
|
return await cacheApi.delete(getCacheName(cacheName));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
return undefined;
|
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;
|
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) {
|
function makeOnProgress(url: string) {
|
||||||
const onProgress: ApiOnProgress = (progress: number) => {
|
const onProgress: ApiOnProgress = (progress: number) => {
|
||||||
progressCallbacks.get(url)?.forEach((callback) => {
|
progressCallbacks.get(url)?.forEach((callback) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user