feat: perf optimisation
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 2s
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 2s
This commit is contained in:
@@ -8,6 +8,8 @@ import logger from "@/lib/logger";
|
||||
interface KomgaRequestInit extends RequestInit {
|
||||
isImage?: boolean;
|
||||
noJson?: boolean;
|
||||
/** Next.js cache duration in seconds. Use false to disable cache, number for TTL */
|
||||
revalidate?: number | false;
|
||||
}
|
||||
|
||||
interface KomgaUrlBuilder {
|
||||
@@ -90,7 +92,8 @@ export abstract class BaseApiService {
|
||||
}
|
||||
|
||||
const isDebug = process.env.KOMGA_DEBUG === "true";
|
||||
const startTime = isDebug ? Date.now() : 0;
|
||||
const isCacheDebug = process.env.CACHE_DEBUG === "true";
|
||||
const startTime = isDebug || isCacheDebug ? Date.now() : 0;
|
||||
|
||||
if (isDebug) {
|
||||
logger.info(
|
||||
@@ -100,11 +103,23 @@ export abstract class BaseApiService {
|
||||
params,
|
||||
isImage: options.isImage,
|
||||
noJson: options.noJson,
|
||||
revalidate: options.revalidate,
|
||||
},
|
||||
"🔵 Komga Request"
|
||||
);
|
||||
}
|
||||
|
||||
if (isCacheDebug && options.revalidate) {
|
||||
logger.info(
|
||||
{
|
||||
url,
|
||||
cache: "enabled",
|
||||
ttl: options.revalidate,
|
||||
},
|
||||
"💾 Cache enabled"
|
||||
);
|
||||
}
|
||||
|
||||
// Timeout de 15 secondes pour éviter les blocages longs
|
||||
const timeoutMs = 15000;
|
||||
const controller = new AbortController();
|
||||
@@ -122,6 +137,10 @@ export abstract class BaseApiService {
|
||||
connectTimeout: timeoutMs,
|
||||
bodyTimeout: timeoutMs,
|
||||
headersTimeout: timeoutMs,
|
||||
// Next.js cache
|
||||
next: options.revalidate !== undefined
|
||||
? { revalidate: options.revalidate }
|
||||
: undefined,
|
||||
});
|
||||
} catch (fetchError: any) {
|
||||
// Gestion spécifique des erreurs DNS
|
||||
@@ -139,6 +158,10 @@ export abstract class BaseApiService {
|
||||
// Force IPv4 si IPv6 pose problème
|
||||
// @ts-ignore
|
||||
family: 4,
|
||||
// Next.js cache
|
||||
next: options.revalidate !== undefined
|
||||
? { revalidate: options.revalidate }
|
||||
: undefined,
|
||||
});
|
||||
} else if (fetchError?.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
|
||||
// Retry automatique sur timeout de connexion (cold start)
|
||||
@@ -152,6 +175,10 @@ export abstract class BaseApiService {
|
||||
connectTimeout: timeoutMs,
|
||||
bodyTimeout: timeoutMs,
|
||||
headersTimeout: timeoutMs,
|
||||
// Next.js cache
|
||||
next: options.revalidate !== undefined
|
||||
? { revalidate: options.revalidate }
|
||||
: undefined,
|
||||
});
|
||||
} else {
|
||||
throw fetchError;
|
||||
@@ -160,8 +187,9 @@ export abstract class BaseApiService {
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (isDebug) {
|
||||
const duration = Date.now() - startTime;
|
||||
logger.info(
|
||||
{
|
||||
url,
|
||||
@@ -173,6 +201,16 @@ export abstract class BaseApiService {
|
||||
);
|
||||
}
|
||||
|
||||
// Log potential cache hit/miss based on response time
|
||||
if (isCacheDebug && options.revalidate) {
|
||||
// Fast response (< 50ms) is likely a cache hit
|
||||
if (duration < 50) {
|
||||
logger.info({ url, duration: `${duration}ms` }, "⚡ Cache HIT (fast response)");
|
||||
} else {
|
||||
logger.info({ url, duration: `${duration}ms` }, "🔄 Cache MISS (slow response)");
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (isDebug) {
|
||||
logger.error(
|
||||
|
||||
@@ -6,12 +6,22 @@ import { ERROR_CODES } from "../../constants/errorCodes";
|
||||
import { AppError } from "../../utils/errors";
|
||||
|
||||
export class BookService extends BaseApiService {
|
||||
private static readonly CACHE_TTL = 60; // 1 minute
|
||||
|
||||
static async getBook(bookId: string): Promise<KomgaBookWithPages> {
|
||||
try {
|
||||
// Récupération parallèle des détails du tome et des pages
|
||||
const [book, pages] = await Promise.all([
|
||||
this.fetchFromApi<KomgaBook>({ path: `books/${bookId}` }),
|
||||
this.fetchFromApi<{ number: number }[]>({ path: `books/${bookId}/pages` }),
|
||||
this.fetchFromApi<KomgaBook>(
|
||||
{ path: `books/${bookId}` },
|
||||
{},
|
||||
{ revalidate: this.CACHE_TTL }
|
||||
),
|
||||
this.fetchFromApi<{ number: number }[]>(
|
||||
{ path: `books/${bookId}/pages` },
|
||||
{},
|
||||
{ revalidate: this.CACHE_TTL }
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -44,7 +54,11 @@ export class BookService extends BaseApiService {
|
||||
|
||||
static async getBookSeriesId(bookId: string): Promise<string> {
|
||||
try {
|
||||
const book = await this.fetchFromApi<KomgaBook>({ path: `books/${bookId}` });
|
||||
const book = await this.fetchFromApi<KomgaBook>(
|
||||
{ path: `books/${bookId}` },
|
||||
{},
|
||||
{ revalidate: this.CACHE_TTL }
|
||||
);
|
||||
return book.seriesId;
|
||||
} catch (error) {
|
||||
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {}, error);
|
||||
|
||||
@@ -8,53 +8,75 @@ import { AppError } from "../../utils/errors";
|
||||
export type { HomeData };
|
||||
|
||||
export class HomeService extends BaseApiService {
|
||||
private static readonly CACHE_TTL = 120; // 2 minutes
|
||||
|
||||
static async getHomeData(): Promise<HomeData> {
|
||||
try {
|
||||
const [ongoing, ongoingBooks, recentlyRead, onDeck, latestSeries] = await Promise.all([
|
||||
this.fetchFromApi<LibraryResponse<KomgaSeries>>({
|
||||
path: "series",
|
||||
params: {
|
||||
read_status: "IN_PROGRESS",
|
||||
sort: "readDate,desc",
|
||||
page: "0",
|
||||
size: "10",
|
||||
media_status: "READY",
|
||||
this.fetchFromApi<LibraryResponse<KomgaSeries>>(
|
||||
{
|
||||
path: "series",
|
||||
params: {
|
||||
read_status: "IN_PROGRESS",
|
||||
sort: "readDate,desc",
|
||||
page: "0",
|
||||
size: "10",
|
||||
media_status: "READY",
|
||||
},
|
||||
},
|
||||
}),
|
||||
this.fetchFromApi<LibraryResponse<KomgaBook>>({
|
||||
path: "books",
|
||||
params: {
|
||||
read_status: "IN_PROGRESS",
|
||||
sort: "readProgress.readDate,desc",
|
||||
page: "0",
|
||||
size: "10",
|
||||
media_status: "READY",
|
||||
{},
|
||||
{ revalidate: this.CACHE_TTL }
|
||||
),
|
||||
this.fetchFromApi<LibraryResponse<KomgaBook>>(
|
||||
{
|
||||
path: "books",
|
||||
params: {
|
||||
read_status: "IN_PROGRESS",
|
||||
sort: "readProgress.readDate,desc",
|
||||
page: "0",
|
||||
size: "10",
|
||||
media_status: "READY",
|
||||
},
|
||||
},
|
||||
}),
|
||||
this.fetchFromApi<LibraryResponse<KomgaBook>>({
|
||||
path: "books/latest",
|
||||
params: {
|
||||
page: "0",
|
||||
size: "10",
|
||||
media_status: "READY",
|
||||
{},
|
||||
{ revalidate: this.CACHE_TTL }
|
||||
),
|
||||
this.fetchFromApi<LibraryResponse<KomgaBook>>(
|
||||
{
|
||||
path: "books/latest",
|
||||
params: {
|
||||
page: "0",
|
||||
size: "10",
|
||||
media_status: "READY",
|
||||
},
|
||||
},
|
||||
}),
|
||||
this.fetchFromApi<LibraryResponse<KomgaBook>>({
|
||||
path: "books/ondeck",
|
||||
params: {
|
||||
page: "0",
|
||||
size: "10",
|
||||
media_status: "READY",
|
||||
{},
|
||||
{ revalidate: this.CACHE_TTL }
|
||||
),
|
||||
this.fetchFromApi<LibraryResponse<KomgaBook>>(
|
||||
{
|
||||
path: "books/ondeck",
|
||||
params: {
|
||||
page: "0",
|
||||
size: "10",
|
||||
media_status: "READY",
|
||||
},
|
||||
},
|
||||
}),
|
||||
this.fetchFromApi<LibraryResponse<KomgaSeries>>({
|
||||
path: "series/latest",
|
||||
params: {
|
||||
page: "0",
|
||||
size: "10",
|
||||
media_status: "READY",
|
||||
{},
|
||||
{ revalidate: this.CACHE_TTL }
|
||||
),
|
||||
this.fetchFromApi<LibraryResponse<KomgaSeries>>(
|
||||
{
|
||||
path: "series/latest",
|
||||
params: {
|
||||
page: "0",
|
||||
size: "10",
|
||||
media_status: "READY",
|
||||
},
|
||||
},
|
||||
}),
|
||||
{},
|
||||
{ revalidate: this.CACHE_TTL }
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -14,18 +14,28 @@ interface KomgaLibraryRaw {
|
||||
}
|
||||
|
||||
export class LibraryService extends BaseApiService {
|
||||
private static readonly CACHE_TTL = 300; // 5 minutes
|
||||
|
||||
static async getLibraries(): Promise<KomgaLibrary[]> {
|
||||
try {
|
||||
const libraries = await this.fetchFromApi<KomgaLibraryRaw[]>({ path: "libraries" });
|
||||
const libraries = await this.fetchFromApi<KomgaLibraryRaw[]>(
|
||||
{ path: "libraries" },
|
||||
{},
|
||||
{ revalidate: this.CACHE_TTL }
|
||||
);
|
||||
|
||||
// Enrich each library with book counts
|
||||
// Enrich each library with book counts (parallel requests)
|
||||
const enrichedLibraries = await Promise.all(
|
||||
libraries.map(async (library) => {
|
||||
try {
|
||||
const booksResponse = await this.fetchFromApi<{ totalElements: number }>({
|
||||
path: "books",
|
||||
params: { library_id: library.id, size: "0" },
|
||||
});
|
||||
const booksResponse = await this.fetchFromApi<{ totalElements: number }>(
|
||||
{
|
||||
path: "books",
|
||||
params: { library_id: library.id, size: "0" },
|
||||
},
|
||||
{},
|
||||
{ revalidate: this.CACHE_TTL }
|
||||
);
|
||||
return {
|
||||
...library,
|
||||
importLastModified: "",
|
||||
@@ -76,40 +86,19 @@ export class LibraryService extends BaseApiService {
|
||||
let condition: any;
|
||||
|
||||
if (unreadOnly) {
|
||||
// Utiliser allOf pour combiner libraryId avec anyOf pour UNREAD ou IN_PROGRESS
|
||||
condition = {
|
||||
allOf: [
|
||||
{
|
||||
libraryId: {
|
||||
operator: "is",
|
||||
value: libraryId,
|
||||
},
|
||||
},
|
||||
{ libraryId: { operator: "is", value: libraryId } },
|
||||
{
|
||||
anyOf: [
|
||||
{
|
||||
readStatus: {
|
||||
operator: "is",
|
||||
value: "UNREAD",
|
||||
},
|
||||
},
|
||||
{
|
||||
readStatus: {
|
||||
operator: "is",
|
||||
value: "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
{ readStatus: { operator: "is", value: "UNREAD" } },
|
||||
{ readStatus: { operator: "is", value: "IN_PROGRESS" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
condition = {
|
||||
libraryId: {
|
||||
operator: "is",
|
||||
value: libraryId,
|
||||
},
|
||||
};
|
||||
condition = { libraryId: { operator: "is", value: libraryId } };
|
||||
}
|
||||
|
||||
const searchBody: { condition: any; fullTextSearch?: string } = { condition };
|
||||
@@ -127,13 +116,10 @@ export class LibraryService extends BaseApiService {
|
||||
const response = await this.fetchFromApi<LibraryResponse<Series>>(
|
||||
{ path: "series/list", params },
|
||||
headers,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(searchBody),
|
||||
}
|
||||
{ method: "POST", body: JSON.stringify(searchBody), revalidate: this.CACHE_TTL }
|
||||
);
|
||||
|
||||
// Filtrer uniquement les séries supprimées côté client (léger)
|
||||
// Filtrer uniquement les séries supprimées
|
||||
const filteredContent = response.content.filter((series) => !series.deleted);
|
||||
|
||||
return {
|
||||
@@ -149,12 +135,9 @@ export class LibraryService extends BaseApiService {
|
||||
static async scanLibrary(libraryId: string, deep: boolean = false): Promise<void> {
|
||||
try {
|
||||
await this.fetchFromApi(
|
||||
{
|
||||
path: `libraries/${libraryId}/scan`,
|
||||
params: { deep: String(deep) },
|
||||
},
|
||||
{ path: `libraries/${libraryId}/scan`, params: { deep: String(deep) } },
|
||||
{},
|
||||
{ method: "POST", noJson: true }
|
||||
{ method: "POST", noJson: true, revalidate: 0 } // bypass cache on mutations
|
||||
);
|
||||
} catch (error) {
|
||||
throw new AppError(ERROR_CODES.LIBRARY.SCAN_ERROR, { libraryId }, error);
|
||||
|
||||
@@ -10,9 +10,15 @@ import type { UserPreferences } from "@/types/preferences";
|
||||
import logger from "@/lib/logger";
|
||||
|
||||
export class SeriesService extends BaseApiService {
|
||||
private static readonly CACHE_TTL = 120; // 2 minutes
|
||||
|
||||
static async getSeries(seriesId: string): Promise<KomgaSeries> {
|
||||
try {
|
||||
return this.fetchFromApi<KomgaSeries>({ path: `series/${seriesId}` });
|
||||
return this.fetchFromApi<KomgaSeries>(
|
||||
{ path: `series/${seriesId}` },
|
||||
{},
|
||||
{ revalidate: this.CACHE_TTL }
|
||||
);
|
||||
} catch (error) {
|
||||
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
|
||||
}
|
||||
@@ -81,6 +87,7 @@ export class SeriesService extends BaseApiService {
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(searchBody),
|
||||
revalidate: this.CACHE_TTL,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user