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
This commit is contained in:
schroda
2023-11-11 23:43:22 +01:00
committed by GitHub
parent 345fbcb5c7
commit a0a52b11a1
4 changed files with 76 additions and 14 deletions

View File

@@ -55,7 +55,11 @@ export const SourceCard: React.FC<IProps> = (props: IProps) => {
margin: '10px',
}}
>
<CardActionArea component={Link} to={`/sources/${id}`} state={{ contentType: SourceContentType.POPULAR }}>
<CardActionArea
component={Link}
to={`/sources/${id}`}
state={{ contentType: SourceContentType.POPULAR, clearCache: true }}
>
<CardContent
sx={{
display: 'flex',
@@ -105,7 +109,7 @@ export const SourceCard: React.FC<IProps> = (props: IProps) => {
variant="outlined"
component={Link}
to={`/sources/${id}`}
state={{ contentType: SourceContentType.LATEST }}
state={{ contentType: SourceContentType.LATEST, clearCache: true }}
>
{t('global.button.latest')}
</Button>
@@ -117,7 +121,7 @@ export const SourceCard: React.FC<IProps> = (props: IProps) => {
variant="outlined"
component={Link}
to={`/sources/${id}`}
state={{ contentType: SourceContentType.LATEST }}
state={{ contentType: SourceContentType.LATEST, clearCache: true }}
>
{t('global.button.latest')}
</Button>
@@ -126,7 +130,7 @@ export const SourceCard: React.FC<IProps> = (props: IProps) => {
variant="outlined"
component={Link}
to={`/sources/${id}`}
state={{ contentType: SourceContentType.POPULAR }}
state={{ contentType: SourceContentType.POPULAR, clearCache: true }}
>
{t('global.button.popular')}
</Button>

View File

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

View File

@@ -295,6 +295,16 @@ export type AbortableApolloMutationResponse<Data = any> = { response: Promise<Fe
const EXTENSION_LIST_CACHE_KEY = 'useExtensionListFetch';
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';
export const SPECIAL_ED_SOURCES = {
REVALIDATION: [
'57122881048805941', // e-hentai
],
};
// TODO - correctly update cache after all mutations instead of refetching queries
export class RequestManager {
public static readonly API_VERSION = '/api/v1/';
@@ -322,6 +332,14 @@ export class RequestManager {
return `${this.getBaseUrl()}${apiVersion}${endpoint}`;
}
public clearBrowseCacheFor(sourceId: string) {
const cacheKeys = this.cache.getMatchingKeys(
new RegExp(`${CACHE_INITIAL_PAGES_FETCHING_KEY}|${CACHE_PAGES_KEY}|${CACHE_RESULTS_KEY}.*${sourceId}`),
);
this.cache.clearFor(...cacheKeys);
}
private createAbortController(): { signal: AbortSignal } & AbortableRequest {
const abortController = new AbortController();
const abortRequest = (reason?: any): void => {
@@ -356,6 +374,7 @@ export class RequestManager {
}
private async revalidatePage<Data = any, Variables extends OperationVariables = OperationVariables>(
sourceId: string,
cacheResultsKey: string,
cachePagesKey: string,
getVariablesFor: (page: number) => Variables,
@@ -369,6 +388,10 @@ export class RequestManager {
maxPage: number,
signal: AbortSignal,
): Promise<void> {
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,

View File

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