206 lines
5.1 KiB
TypeScript

import type { MarketApiResult } from "./api-client";
import { createMarketCacheStore } from "./cache-store";
import type { RowErrorReason, MarketRowState } from "./row-state";
import type { RequiredAfterSearchRates } from "./types";
interface BatchLoaderRow {
authorId: string | null;
render(
state: MarketRowState,
options?: { onRetry?: () => Promise<void> | void }
): void;
}
interface LoadRowsOptions {
listSeq: number;
rows: BatchLoaderRow[];
shouldRenderResult?: (params: {
authorId: string;
listSeq: number;
row: BatchLoaderRow;
}) => boolean;
}
interface MarketBatchLoaderOptions {
apiClient: {
loadAuthorAseInfo(authorId: string): Promise<MarketApiResult>;
};
cacheStore?: ReturnType<typeof createMarketCacheStore>;
concurrency?: number;
}
export function createMarketBatchLoader(options: MarketBatchLoaderOptions) {
const cacheStore = options.cacheStore ?? createMarketCacheStore();
const concurrency = Math.max(options.concurrency ?? 4, 1);
return {
async loadRows(loadOptions: LoadRowsOptions) {
const groupedRows = new Map<string, BatchLoaderRow[]>();
for (const row of loadOptions.rows) {
if (!row.authorId) {
row.render({
authorId: null,
listSeq: loadOptions.listSeq,
reason: "missing-author-id",
retryable: false,
state: "error"
});
continue;
}
const cached = cacheStore.getSuccess(row.authorId);
if (cached) {
row.render({
authorId: row.authorId,
listSeq: loadOptions.listSeq,
personalVideoAfterSearchRate: cached.rates.personalVideoAfterSearchRate,
singleVideoAfterSearchRate: cached.rates.singleVideoAfterSearchRate,
source: "cache",
state: "success"
});
continue;
}
row.render({
authorId: row.authorId,
listSeq: loadOptions.listSeq,
state: "loading"
});
const rowsForAuthor = groupedRows.get(row.authorId) ?? [];
rowsForAuthor.push(row);
groupedRows.set(row.authorId, rowsForAuthor);
}
const tasks = Array.from(groupedRows.entries(), ([authorId, rows]) => {
return () => loadAuthorRows(authorId, rows, loadOptions);
});
await runWithConcurrency(tasks, concurrency);
}
};
async function loadAuthorRows(
authorId: string,
rows: BatchLoaderRow[],
loadOptions: LoadRowsOptions
) {
const result = await requestAuthor(authorId);
for (const row of rows) {
if (
loadOptions.shouldRenderResult &&
!loadOptions.shouldRenderResult({
authorId,
listSeq: loadOptions.listSeq,
row
})
) {
continue;
}
renderAuthorResult(row, authorId, loadOptions.listSeq, result, false);
}
}
async function retryRow(row: BatchLoaderRow, listSeq: number) {
if (!row.authorId) {
return;
}
row.render({
authorId: row.authorId,
listSeq,
state: "loading"
});
const result = await requestAuthor(row.authorId);
renderAuthorResult(row, row.authorId, listSeq, result, false);
}
async function requestAuthor(authorId: string): Promise<MarketApiResult> {
const cached = cacheStore.getSuccess(authorId);
if (cached) {
return {
rates: cached.rates,
success: true
};
}
const inflight = cacheStore.getInflight<MarketApiResult>(authorId);
if (inflight) {
return inflight;
}
const requestPromise = options.apiClient
.loadAuthorAseInfo(authorId)
.then((result) => {
if (result.success) {
cacheStore.setSuccess(authorId, result.rates);
}
return result;
})
.finally(() => {
cacheStore.clearInflight(authorId);
});
cacheStore.setInflight(authorId, requestPromise);
return requestPromise;
}
function renderAuthorResult(
row: BatchLoaderRow,
authorId: string,
listSeq: number,
result: MarketApiResult,
fromCache: boolean
) {
if (result.success) {
const rates = result.rates as RequiredAfterSearchRates;
row.render({
authorId,
listSeq,
personalVideoAfterSearchRate: rates.personalVideoAfterSearchRate,
singleVideoAfterSearchRate: rates.singleVideoAfterSearchRate,
source: fromCache ? "cache" : "network",
state: "success"
});
return;
}
row.render(
{
authorId,
listSeq,
reason: result.reason as RowErrorReason,
retryable: true,
state: "error"
},
{
onRetry: () => retryRow(row, listSeq)
}
);
}
}
async function runWithConcurrency(
tasks: Array<() => Promise<void>>,
concurrency: number
) {
let nextTaskIndex = 0;
const workers = Array.from(
{ length: Math.min(concurrency, tasks.length) },
async () => {
while (nextTaskIndex < tasks.length) {
const task = tasks[nextTaskIndex];
nextTaskIndex += 1;
await task();
}
}
);
await Promise.all(workers);
}