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:
@@ -16,6 +16,7 @@ import RefreshIcon from '@mui/icons-material/Refresh';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||||
|
import { Priority } from '@/lib/Queue.ts';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
src: string;
|
src: string;
|
||||||
@@ -48,7 +49,7 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let tmpImageSourceUrl: string;
|
let tmpImageSourceUrl: string;
|
||||||
const imageRequest = requestManager.requestImage(src);
|
const imageRequest = requestManager.requestImage(src, Priority.HIGH);
|
||||||
let cacheTimeout: NodeJS.Timeout;
|
let cacheTimeout: NodeJS.Timeout;
|
||||||
|
|
||||||
const fetchImage = async () => {
|
const fetchImage = async () => {
|
||||||
|
|||||||
30
src/lib/ControlledPromise.ts
Normal file
30
src/lib/ControlledPromise.ts
Normal 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
75
src/lib/Queue.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,6 @@ import {
|
|||||||
} from '@apollo/client';
|
} from '@apollo/client';
|
||||||
import { OperationVariables } from '@apollo/client/core';
|
import { OperationVariables } from '@apollo/client/core';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import pLimit from 'p-limit';
|
|
||||||
import { IRestClient, RestClient } from '@/lib/requests/client/RestClient.ts';
|
import { IRestClient, RestClient } from '@/lib/requests/client/RestClient.ts';
|
||||||
import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts';
|
import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts';
|
||||||
import {
|
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 { WEBUI_UPDATE_SUBSCRIPTION } from '@/lib/graphql/subscriptions/ServerInfoSubscription.ts';
|
||||||
import { GET_DOWNLOAD_STATUS } from '@/lib/graphql/queries/DownloaderQuery.ts';
|
import { GET_DOWNLOAD_STATUS } from '@/lib/graphql/queries/DownloaderQuery.ts';
|
||||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||||
|
import { Queue, QueuePriority } from '@/lib/Queue.ts';
|
||||||
|
|
||||||
enum GQLMethod {
|
enum GQLMethod {
|
||||||
QUERY = 'QUERY',
|
QUERY = 'QUERY',
|
||||||
@@ -366,7 +366,7 @@ export class RequestManager {
|
|||||||
|
|
||||||
private readonly cache = new CustomCache();
|
private readonly cache = new CustomCache();
|
||||||
|
|
||||||
private readonly imageQueue = pLimit(5);
|
private readonly imageQueue = new Queue(5);
|
||||||
|
|
||||||
public getClient(): IRestClient {
|
public getClient(): IRestClient {
|
||||||
return this.restClient;
|
return this.restClient;
|
||||||
@@ -768,20 +768,23 @@ export class RequestManager {
|
|||||||
* img.src = imageUrl;
|
* 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 { abortRequest, signal } = this.createAbortController();
|
||||||
const response = this.imageQueue(() =>
|
const response = this.imageQueue.enqueue(
|
||||||
this.restClient
|
url,
|
||||||
.fetcher(url, {
|
() =>
|
||||||
checkResponseIsJson: false,
|
this.restClient
|
||||||
config: {
|
.fetcher(url, {
|
||||||
signal,
|
checkResponseIsJson: false,
|
||||||
// @ts-ignore - typing has not been updated yet
|
config: {
|
||||||
priority: 'low',
|
signal,
|
||||||
},
|
// @ts-ignore - typing has not been updated yet
|
||||||
})
|
priority: 'low',
|
||||||
.then((data) => data.blob())
|
},
|
||||||
.then((data) => URL.createObjectURL(data)),
|
})
|
||||||
|
.then((data) => data.blob())
|
||||||
|
.then((data) => URL.createObjectURL(data)),
|
||||||
|
priority,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { response, abortRequest };
|
return { response, abortRequest };
|
||||||
|
|||||||
Reference in New Issue
Block a user