From a0a52b11a1260efc4a1532b34291e22f6bef7933 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sat, 11 Nov 2023 23:43:22 +0100 Subject: [PATCH] Fix/pagination of sources which require pages to be fetched in order (#451) * Handle sources that do not support revalidation At least e-hentai is not able to handle out of order pagination requests. E.g. in case the last request was for page 1 and the next one is for page 3, the result will be for page 2, because e-hentai just ignores the request page number and just returns the next page after the last fetched one. * Optionally clear source browse cache when opening component At least e-hentai is not able to handle out of order pagination requests. The revalidation logic will cause exactly this. Due to this, the revalidation for these sources is disabled. To prevent an out of date cache, it gets cleared when routing to the source component --- src/components/SourceCard.tsx | 12 +++++++---- src/lib/requests/CustomCache.ts | 15 +++++++++++++ src/lib/requests/RequestManager.ts | 29 +++++++++++++++++++++---- src/screens/SourceMangas.tsx | 34 ++++++++++++++++++++++++------ 4 files changed, 76 insertions(+), 14 deletions(-) diff --git a/src/components/SourceCard.tsx b/src/components/SourceCard.tsx index 795011d3df..a34bbd10d4 100644 --- a/src/components/SourceCard.tsx +++ b/src/components/SourceCard.tsx @@ -55,7 +55,11 @@ export const SourceCard: React.FC = (props: IProps) => { margin: '10px', }} > - + = (props: IProps) => { variant="outlined" component={Link} to={`/sources/${id}`} - state={{ contentType: SourceContentType.LATEST }} + state={{ contentType: SourceContentType.LATEST, clearCache: true }} > {t('global.button.latest')} @@ -117,7 +121,7 @@ export const SourceCard: React.FC = (props: IProps) => { variant="outlined" component={Link} to={`/sources/${id}`} - state={{ contentType: SourceContentType.LATEST }} + state={{ contentType: SourceContentType.LATEST, clearCache: true }} > {t('global.button.latest')} @@ -126,7 +130,7 @@ export const SourceCard: React.FC = (props: IProps) => { variant="outlined" component={Link} to={`/sources/${id}`} - state={{ contentType: SourceContentType.POPULAR }} + state={{ contentType: SourceContentType.POPULAR, clearCache: true }} > {t('global.button.popular')} diff --git a/src/lib/requests/CustomCache.ts b/src/lib/requests/CustomCache.ts index 4fe4cca12a..803dd8e2e5 100644 --- a/src/lib/requests/CustomCache.ts +++ b/src/lib/requests/CustomCache.ts @@ -43,6 +43,21 @@ export class CustomCache { return this.keyToResponseMap.get(key) as Response; } + public getAllKeys(): string[] { + return [...this.keyToResponseMap.keys()]; + } + + public getMatchingKeys(regex: RegExp): string[] { + return this.getAllKeys().filter((key) => !!regex.exec(key)); + } + + public clearFor(...keys: string[]) { + keys.forEach((key) => { + this.keyToResponseMap.delete(key); + this.keyToFetchTimestampMap.delete(key); + }); + } + public clear(): void { this.keyToResponseMap.clear(); this.keyToFetchTimestampMap.clear(); diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index b289ca3900..77a48dab1f 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -295,6 +295,16 @@ export type AbortableApolloMutationResponse = { response: Promise { @@ -356,6 +374,7 @@ export class RequestManager { } private async revalidatePage( + sourceId: string, cacheResultsKey: string, cachePagesKey: string, getVariablesFor: (page: number) => Variables, @@ -369,6 +388,10 @@ export class RequestManager { maxPage: number, signal: AbortSignal, ): Promise { + if (SPECIAL_ED_SOURCES.REVALIDATION.includes(sourceId)) { + return; + } + const { response: revalidationRequest } = this.doRequest( GQLMethod.MUTATION, GET_SOURCE_MANGAS_FETCH, @@ -406,6 +429,7 @@ export class RequestManager { if (isCachedPageInvalid && pageToRevalidate < maxPage) { await this.revalidatePage( + sourceId, cacheResultsKey, cachePagesKey, getVariablesFor, @@ -965,10 +989,6 @@ export class RequestManager { }, }); - const CACHE_INITIAL_PAGES_FETCHING_KEY = 'GET_SOURCE_MANGAS_FETCH_FETCHING_INITIAL_PAGES'; - const CACHE_PAGES_KEY = 'GET_SOURCE_MANGAS_FETCH_PAGES'; - const CACHE_RESULTS_KEY = 'GET_SOURCE_MANGAS_FETCH'; - const isRevalidationDoneRef = useRef(false); const activeRevalidationRef = useRef< | [ @@ -1019,6 +1039,7 @@ export class RequestManager { const revalidatePage = async (pageToRevalidate: number, maxPage: number, signal: AbortSignal) => this.revalidatePage( + input.source, CACHE_RESULTS_KEY, CACHE_PAGES_KEY, getVariablesFor, diff --git a/src/screens/SourceMangas.tsx b/src/screens/SourceMangas.tsx index 49400e63d1..3bfb24390e 100644 --- a/src/screens/SourceMangas.tsx +++ b/src/screens/SourceMangas.tsx @@ -18,7 +18,11 @@ import FavoriteIcon from '@mui/icons-material/Favorite'; import NewReleasesIcon from '@mui/icons-material/NewReleases'; import FilterListIcon from '@mui/icons-material/FilterList'; import { TPartialManga, TranslationKey } from '@/typings'; -import { requestManager, AbortableApolloUseMutationPaginatedResponse } from '@/lib/requests/RequestManager.ts'; +import { + requestManager, + AbortableApolloUseMutationPaginatedResponse, + SPECIAL_ED_SOURCES, +} from '@/lib/requests/RequestManager.ts'; import { useDebounce } from '@/components/manga/hooks'; import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext'; import { SourceGridLayout } from '@/components/source/GridLayouts'; @@ -203,11 +207,12 @@ export function SourceMangas() { const { contentType: currentLocationContentType = SourceContentType.POPULAR, filtersToApply: currentLocationFiltersToApply = [], - } = - useLocation<{ - contentType: SourceContentType; - filtersToApply: IPos[]; - }>().state ?? {}; + clearCache = false, + } = useLocation<{ + contentType: SourceContentType; + filtersToApply: IPos[]; + clearCache: boolean; + }>().state ?? {}; const { options } = useLibraryOptionsContext(); const [query] = useQueryParam('query', StringParam); @@ -288,6 +293,23 @@ export function SourceMangas() { setResetScrollPosition(true); }, [sourceId, contentType]); + useEffect(() => { + if (!clearCache) { + return; + } + + const requiresClear = SPECIAL_ED_SOURCES.REVALIDATION.includes(sourceId); + if (!requiresClear) { + return; + } + + requestManager.clearBrowseCacheFor(sourceId); + navigate('', { + replace: true, + state: { contentType: currentLocationContentType, filters: currentLocationFiltersToApply }, + }); + }, [clearCache]); + useEffect( () => () => { if (contentType !== SourceContentType.SEARCH) {