Add prioritization to image requests (#671)

In the reader the preloaded page requests were triggered before the actual page got rendered and thus, the image request of the rendered page had to wait for all the preloaded images to finish
This commit is contained in:
schroda
2024-03-24 19:08:36 +01:00
committed by GitHub
parent 34aeac9c33
commit 06b2bca340
4 changed files with 125 additions and 16 deletions

View File

@@ -16,6 +16,7 @@ import RefreshIcon from '@mui/icons-material/Refresh';
import { useTranslation } from 'react-i18next';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { Priority } from '@/lib/Queue.ts';
interface IProps {
src: string;
@@ -48,7 +49,7 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
useEffect(() => {
let tmpImageSourceUrl: string;
const imageRequest = requestManager.requestImage(src);
const imageRequest = requestManager.requestImage(src, Priority.HIGH);
let cacheTimeout: NodeJS.Timeout;
const fetchImage = async () => {

View File

@@ -0,0 +1,30 @@
/*
* 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/.
*/
export class ControlledPromise<T = void> {
private orgResolve!: (value: T | PromiseLike<T>) => void;
private orgReject!: (reason?: any) => void;
public readonly promise: Promise<T>;
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.orgResolve = resolve;
this.orgReject = reject;
});
}
resolve(value: T): void {
this.orgResolve(value);
}
reject(reason?: any): void {
this.orgReject(reason);
}
}

75
src/lib/Queue.ts Normal file
View File

@@ -0,0 +1,75 @@
/*
* 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 pLimit, { LimitFunction } from 'p-limit';
import { ControlledPromise } from '@/lib/ControlledPromise.ts';
export enum Priority {
LOW = 0,
NORMAL = 1,
HIGH = 2,
}
export type QueuePriority = Priority | number;
type Key = string;
type QueueItemFunction<T = any> = () => PromiseLike<T> | T;
export class Queue {
private readonly queue: LimitFunction;
private counter: number = 0;
private pendingKeyToPriorityMap = new Map<Key, QueuePriority>();
private pendingKeyToFnMap = new Map<Key, QueueItemFunction>();
private pendingKeyToPromiseMap = new Map<Key, ControlledPromise<any>>();
constructor(concurrency: number) {
this.queue = pLimit(concurrency);
}
enqueue<T>(key: Key, fn: () => PromiseLike<T> | T, priority: QueuePriority = Priority.NORMAL): Promise<T> {
this.counter = (this.counter + 1) % Infinity;
const actualKey = `${key}_${this.counter}`;
this.pendingKeyToPriorityMap.set(actualKey, priority);
this.pendingKeyToFnMap.set(actualKey, fn);
const processPromise = new ControlledPromise<T>();
this.pendingKeyToPromiseMap.set(actualKey, processPromise);
this.queue(() => this.process());
return processPromise.promise;
}
private async process(): Promise<void> {
const { fn, promise } = this.getNextItemToProcess();
try {
const result = await fn();
promise.resolve(result);
} catch (e) {
promise.reject(e);
}
}
private getNextItemToProcess<T>(): { key: Key; fn: QueueItemFunction<T>; promise: ControlledPromise<T> } {
const [key] = [...this.pendingKeyToPriorityMap.entries()].toSorted(
([, priorityA], [, priorityB]) => priorityB - priorityA,
)[0];
const fn = this.pendingKeyToFnMap.get(key) as () => PromiseLike<T> | T;
const promise = this.pendingKeyToPromiseMap.get(key) as ControlledPromise<T>;
this.pendingKeyToPriorityMap.delete(key);
this.pendingKeyToFnMap.delete(key);
this.pendingKeyToPromiseMap.delete(key);
return { key, fn, promise };
}
}

View File

@@ -27,7 +27,6 @@ import {
} from '@apollo/client';
import { OperationVariables } from '@apollo/client/core';
import { useEffect, useMemo, useRef, useState } from 'react';
import pLimit from 'p-limit';
import { IRestClient, RestClient } from '@/lib/requests/client/RestClient.ts';
import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts';
import {
@@ -257,6 +256,7 @@ 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 '@/util/defaultPromiseErrorHandler.ts';
import { Queue, QueuePriority } from '@/lib/Queue.ts';
enum GQLMethod {
QUERY = 'QUERY',
@@ -366,7 +366,7 @@ export class RequestManager {
private readonly cache = new CustomCache();
private readonly imageQueue = pLimit(5);
private readonly imageQueue = new Queue(5);
public getClient(): IRestClient {
return this.restClient;
@@ -768,20 +768,23 @@ export class RequestManager {
* img.src = imageUrl;
*
*/
public requestImage(url: string): { response: Promise<string> } & AbortableRequest {
public requestImage(url: string, priority?: QueuePriority): { response: Promise<string> } & AbortableRequest {
const { abortRequest, signal } = this.createAbortController();
const response = this.imageQueue(() =>
this.restClient
.fetcher(url, {
checkResponseIsJson: false,
config: {
signal,
// @ts-ignore - typing has not been updated yet
priority: 'low',
},
})
.then((data) => data.blob())
.then((data) => URL.createObjectURL(data)),
const response = this.imageQueue.enqueue(
url,
() =>
this.restClient
.fetcher(url, {
checkResponseIsJson: false,
config: {
signal,
// @ts-ignore - typing has not been updated yet
priority: 'low',
},
})
.then((data) => data.blob())
.then((data) => URL.createObjectURL(data)),
priority,
);
return { response, abortRequest };