Array of search queries with alias, title, and index.
Configuration including auth token, page size, abort signal.
Promise resolving to map of search results keyed by alias.
export async function batchSearchManga(
searches: Array<{ alias: string; title: string; index: number }>,
options: {
token?: string;
perPage?: number;
abortSignal?: AbortSignal;
noRetry?: boolean;
} = {},
): Promise<
Map<
string,
{
media: AniListManga[];
index: number;
title: string;
}
>
> {
return withGroupAsync(
`[AniListClient] Batch Search (${searches.length} queries)`,
async () => {
if (searches.length === 0) {
return new Map();
}
const { token, perPage = 10, abortSignal, noRetry } = options;
console.info(
`[AniListClient] 🚀 Batch searching ${searches.length} manga titles`,
);
// Build the batched query dynamically
const queryParts: string[] = [];
for (const { alias, title } of searches) {
// Sanitize the title for use in GraphQL (escape quotes)
const sanitizedTitle = JSON.stringify(title).slice(1, -1);
queryParts.push(`
${alias}: Page(page: 1, perPage: ${perPage}) {
pageInfo {
total
currentPage
lastPage
hasNextPage
perPage
}
media(type: MANGA, search: "${sanitizedTitle}", format_not_in: [NOVEL]) {
id
title {
romaji
english
native
}
synonyms
format
status
chapters
volumes
description
genres
tags {
id
name
category
}
countryOfOrigin
source
staff {
edges {
role
node {
name {
full
}
}
}
}
coverImage {
large
medium
}
mediaListEntry {
id
status
progress
score
private
}
isAdult
}
}`);
}
const batchedQuery = `
query BatchSearchManga {
${queryParts.join("\n")}
}
`;
type BatchQueryResponse = Record<
string,
{
media?: AniListManga[];
pageInfo?: unknown;
}
>;
try {
// Execute the batched request
const response = await request<BatchQueryResponse>(
batchedQuery,
{}, // No variables needed - all values are in the query
token,
abortSignal,
true, // Bypass cache for batch requests
noRetry,
);
console.debug(`[AniListClient] 🔍 Batch search response:`, response);
// Validate response structure
if (!response?.data) {
console.error(
`[AniListClient] ❌ Invalid API response for batch search:`,
response,
);
throw new Error(`Invalid API response: missing data property`);
}
// Handle nested data structure and type assertion
const responseData = (response.data.data ??
response.data) as unknown as BatchQueryResponse;
// Process results into a map
const results = new Map<
string,
{
media: AniListManga[];
index: number;
title: string;
}
>();
let totalResults = 0;
for (const { alias, index, title } of searches) {
const aliasData = responseData[alias];
if (aliasData?.media?.length) {
results.set(alias, {
media: aliasData.media,
index,
title,
});
totalResults += aliasData.media.length;
} else {
// Return empty array if no results
results.set(alias, {
media: [],
index,
title,
});
}
}
console.info(
`[AniListClient] ✅ Batch search complete: ${totalResults} total results for ${searches.length} queries`,
);
return results;
} catch (error) {
captureError(
ErrorType.NETWORK,
`Error in batch search for ${searches.length} queries`,
error instanceof Error ? error : new Error(String(error)),
{ queryCount: searches.length },
);
// Return empty results for all searches on error
const emptyResults = new Map<
string,
{
media: AniListManga[];
index: number;
title: string;
}
>();
for (const { alias, index, title } of searches) {
emptyResults.set(alias, {
media: [],
index,
title,
});
}
return emptyResults;
}
},
);
}
Batch search for multiple manga titles in a single GraphQL request. More efficient than individual searches when matching multiple titles.