Queue image requests per source
If http protocol is h2 or newer, allow: - 5 requests per source - unlimited local source requests
This commit is contained in:
64
src/lib/SourceAwareQueue.ts
Normal file
64
src/lib/SourceAwareQueue.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Queue, type QueuePriority } from '@/lib/Queue';
|
||||
import { Sources } from '@/features/source/services/Sources.ts';
|
||||
|
||||
export class SourceAwareQueue {
|
||||
private static readonly DEFAULT_ID = '__global__';
|
||||
|
||||
private readonly queueBySource = new Map<string, Queue>([
|
||||
[SourceAwareQueue.DEFAULT_ID, new Queue(this.concurrencyPerSource)],
|
||||
]);
|
||||
|
||||
constructor(
|
||||
private readonly areConnectionsLimited: boolean,
|
||||
private readonly concurrencyPerSource = 5,
|
||||
) {}
|
||||
|
||||
private getConcurrencyFor(sourceId: string | null): number {
|
||||
const isLocalSource = sourceId === Sources.LOCAL_SOURCE_ID;
|
||||
|
||||
if (isLocalSource) {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
return this.concurrencyPerSource;
|
||||
}
|
||||
|
||||
private getQueueFor(sourceId: string | null): Queue {
|
||||
const finalSourceId = sourceId ?? SourceAwareQueue.DEFAULT_ID;
|
||||
const queueKey = this.areConnectionsLimited ? SourceAwareQueue.DEFAULT_ID : finalSourceId;
|
||||
|
||||
if (!this.queueBySource.has(queueKey)) {
|
||||
this.queueBySource.set(queueKey, new Queue(this.getConcurrencyFor(finalSourceId)));
|
||||
}
|
||||
|
||||
return this.queueBySource.get(queueKey)!;
|
||||
}
|
||||
|
||||
enqueue<T>(sourceId: string | null, key: string, fn: () => PromiseLike<T> | T, priority?: QueuePriority) {
|
||||
const queue = this.getQueueFor(sourceId);
|
||||
|
||||
return queue.enqueue(key, fn, priority);
|
||||
}
|
||||
|
||||
isProcessing(sourceId: string | null, key: string): boolean {
|
||||
const queue = this.getQueueFor(sourceId);
|
||||
|
||||
return queue.isProcessing(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
for (const queue of this.queueBySource.values()) {
|
||||
queue.clear();
|
||||
}
|
||||
|
||||
this.queueBySource.clear();
|
||||
}
|
||||
}
|
||||
45
src/lib/UrlUtil.ts
Normal file
45
src/lib/UrlUtil.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { SearchParam } from '@/base/Base.types.ts';
|
||||
|
||||
export class UrlUtil {
|
||||
static createParams(params: Record<SearchParam | string, string | null | undefined>): URLSearchParams {
|
||||
const paramEntries = Object.entries(params).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
||||
);
|
||||
|
||||
return new URLSearchParams(paramEntries);
|
||||
}
|
||||
|
||||
static createTabParam(tab: string | null | undefined): { [SearchParam.TAB]: string } {
|
||||
return { ...this.createParams({ [SearchParam.TAB]: tab }).entries() } as unknown as {
|
||||
[SearchParam.TAB]: string;
|
||||
};
|
||||
}
|
||||
|
||||
static createQueryParam(query: string | null | undefined): { [SearchParam.QUERY]: string } {
|
||||
return { ...this.createParams({ [SearchParam.QUERY]: query }).entries() } as unknown as {
|
||||
[SearchParam.QUERY]: string;
|
||||
};
|
||||
}
|
||||
|
||||
static addParams(path: string, params: Record<SearchParam | string, string | null | undefined>): string {
|
||||
const urlParams = this.createParams(params).toString();
|
||||
|
||||
if (urlParams) {
|
||||
return `${path}?${urlParams}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
static addQueryParam(path: string, query: string | null | undefined): string {
|
||||
return this.addParams(path, this.createQueryParam(query));
|
||||
}
|
||||
}
|
||||
@@ -315,7 +315,8 @@ import { RESET_WEBUI_UPDATE_STATUS, UPDATE_WEBUI } from '@/lib/graphql/mutations
|
||||
import { WEBUI_UPDATE_SUBSCRIPTION } from '@/lib/graphql/subscriptions/ServerInfoSubscription.ts';
|
||||
import { GET_DOWNLOAD_STATUS } from '@/lib/graphql/queries/DownloaderQuery.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { Queue, QueuePriority } from '@/lib/Queue.ts';
|
||||
import { QueuePriority } from '@/lib/Queue.ts';
|
||||
import { SourceAwareQueue } from '@/lib/SourceAwareQueue.ts';
|
||||
import { TRACKER_SEARCH } from '@/lib/graphql/queries/TrackerQuery.ts';
|
||||
import {
|
||||
TRACKER_BIND,
|
||||
@@ -460,14 +461,29 @@ export class RequestManager {
|
||||
|
||||
private readonly cache = new CustomCache();
|
||||
|
||||
private readonly imageQueue = new Queue(5);
|
||||
private readonly imageQueue: SourceAwareQueue;
|
||||
|
||||
constructor() {
|
||||
const isHttps = typeof window !== 'undefined' && window.location.protocol === 'https:';
|
||||
const isHttp2 = isHttps && this.detectHttp2();
|
||||
|
||||
this.imageQueue = new SourceAwareQueue(isHttp2, 5);
|
||||
|
||||
BaseClient.setTokenRefreshCompleteCallback(() => {
|
||||
this.processQueues();
|
||||
});
|
||||
}
|
||||
|
||||
private detectHttp2(): boolean {
|
||||
const entries = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[];
|
||||
|
||||
if (!entries.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ['h2', 'h3'].includes(entries[0].nextHopProtocol);
|
||||
}
|
||||
|
||||
public getClient(): IRestClient {
|
||||
return this.restClient;
|
||||
}
|
||||
@@ -896,8 +912,8 @@ export class RequestManager {
|
||||
* will just cause new source image requests to be sent to the server, which then will cause the server
|
||||
* to become really slow for image requests to the same source
|
||||
*/
|
||||
private abortImageRequest(key: string, abort: () => void): void {
|
||||
if (this.imageQueue.isProcessing(key)) {
|
||||
private abortImageRequest(key: string, sourceId: string | null, abort: () => void): void {
|
||||
if (this.imageQueue.isProcessing(sourceId, key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -936,12 +952,23 @@ export class RequestManager {
|
||||
return url;
|
||||
}
|
||||
|
||||
private getSourceIdFromUrl(url: string): string | null {
|
||||
try {
|
||||
console.log('asdf', new URL(url).searchParams.get('sourceId'));
|
||||
return new URL(url).searchParams.get('sourceId');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async maybeEnqueueImageRequest<T>(
|
||||
url: string,
|
||||
request: () => Promise<T>,
|
||||
priority?: QueuePriority,
|
||||
ignoreQueue?: boolean,
|
||||
): Promise<ReturnType<typeof this.imageQueue.enqueue<T>> & { fromCache?: boolean }> {
|
||||
const sourceId = this.getSourceIdFromUrl(url);
|
||||
|
||||
try {
|
||||
const isCached = await ImageCache.has(url);
|
||||
if (!!ignoreQueue || isCached) {
|
||||
@@ -952,9 +979,9 @@ export class RequestManager {
|
||||
};
|
||||
}
|
||||
|
||||
return this.imageQueue.enqueue(url, request, priority);
|
||||
return this.imageQueue.enqueue(sourceId, url, request, priority);
|
||||
} catch (error) {
|
||||
return this.imageQueue.enqueue(url, request, priority);
|
||||
return this.imageQueue.enqueue(sourceId, url, request, priority);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1009,7 +1036,8 @@ export class RequestManager {
|
||||
|
||||
return {
|
||||
response,
|
||||
abortRequest: (reason?: any) => this.abortImageRequest(key, () => abortRequest(reason)),
|
||||
abortRequest: (reason?: any) =>
|
||||
this.abortImageRequest(key, this.getSourceIdFromUrl(url), () => abortRequest(reason)),
|
||||
cleanup: () => {},
|
||||
fromCache: !!fromCache,
|
||||
};
|
||||
@@ -1065,7 +1093,8 @@ export class RequestManager {
|
||||
|
||||
return {
|
||||
response,
|
||||
abortRequest: (reason?: any) => this.abortImageRequest(key, () => abortRequest(reason)),
|
||||
abortRequest: (reason?: any) =>
|
||||
this.abortImageRequest(key, this.getSourceIdFromUrl(url), () => abortRequest(reason)),
|
||||
cleanup: () => URL.revokeObjectURL(objectUrl),
|
||||
fromCache: !!fromCache,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user