feat: refactor token management and enhance logging in fetchKeepaDataBatch function

This commit is contained in:
Victor Noguera
2026-04-08 00:35:32 -04:00
parent a5a2e9182c
commit 2e626ce1f3

View File

@@ -22,15 +22,20 @@ async function waitForToken(): Promise<void> {
} }
// Wait until we regenerate at least 1 token // Wait until we regenerate at least 1 token
const waitMs = Math.ceil((1 / refillRate) * 60_000) - (Date.now() - lastRequestTime); const waitMs =
Math.ceil((1 / refillRate) * 60_000) - (Date.now() - lastRequestTime);
if (waitMs > 0) { if (waitMs > 0) {
console.log(`Keepa tokens exhausted. Waiting ${Math.ceil(waitMs / 1000)}s for token regeneration...`); console.log(
`Keepa tokens exhausted. Waiting ${Math.ceil(waitMs / 1000)}s for token regeneration...`,
);
await new Promise((r) => setTimeout(r, waitMs)); await new Promise((r) => setTimeout(r, waitMs));
} }
tokensLeft = 1; tokensLeft = 1;
} }
export async function fetchKeepaDataBatch(asins: string[]): Promise<Map<string, KeepaData>> { export async function fetchKeepaDataBatch(
asins: string[],
): Promise<Map<string, KeepaData>> {
const results = new Map<string, KeepaData>(); const results = new Map<string, KeepaData>();
// Split into chunks of MAX_ASINS_PER_REQUEST // Split into chunks of MAX_ASINS_PER_REQUEST
@@ -41,7 +46,9 @@ export async function fetchKeepaDataBatch(asins: string[]): Promise<Map<string,
const asinParam = chunk.join(","); const asinParam = chunk.join(",");
const url = `${KEEPA_BASE}/product?key=${config.keepaApiKey}&domain=1&asin=${asinParam}&stats=90`; const url = `${KEEPA_BASE}/product?key=${config.keepaApiKey}&domain=1&asin=${asinParam}&stats=90`;
console.log(`Keepa: fetching ${chunk.length} ASINs (tokens left: ${tokensLeft})...`); console.log(
`Keepa: fetching ${chunk.length} ASINs (tokens left: ${tokensLeft})...`,
);
const res = await fetch(url); const res = await fetch(url);
lastRequestTime = Date.now(); lastRequestTime = Date.now();
@@ -61,7 +68,9 @@ export async function fetchKeepaDataBatch(asins: string[]): Promise<Map<string,
if (data.tokensLeft != null) tokensLeft = data.tokensLeft; if (data.tokensLeft != null) tokensLeft = data.tokensLeft;
if (data.refillRate != null) refillRate = data.refillRate; if (data.refillRate != null) refillRate = data.refillRate;
console.log(`Keepa: ${data.products?.length ?? 0} products returned, ${tokensLeft} tokens remaining (refill: ${refillRate}/min)`); console.log(
`Keepa: ${data.products?.length ?? 0} products returned, ${tokensLeft} tokens remaining (refill: ${refillRate}/min)`,
);
if (data.products) { if (data.products) {
for (const product of data.products) { for (const product of data.products) {
@@ -78,6 +87,16 @@ export async function fetchKeepaDataBatch(asins: string[]): Promise<Map<string,
function parseKeepaProduct(product: Record<string, any>): KeepaData { function parseKeepaProduct(product: Record<string, any>): KeepaData {
const stats = product.stats; const stats = product.stats;
const csv = product.csv; const csv = product.csv;
const salesRankDrops30 = pickKeepaNumber(
product.salesRankDrops30,
stats?.salesRankDrops30,
);
const salesRankDrops90 =
pickKeepaNumber(product.salesRankDrops90, stats?.salesRankDrops90) ??
(salesRankDrops30 != null ? salesRankDrops30 * 3 : null);
const monthlySold =
pickKeepaNumber(product.monthlySold, stats?.monthlySold) ??
salesRankDrops30;
return { return {
currentPrice: extractCurrentPrice(csv), currentPrice: extractCurrentPrice(csv),
@@ -86,16 +105,27 @@ function parseKeepaProduct(product: Record<string, any>): KeepaData {
maxPrice90: stats?.max?.[0] != null ? stats.max[0] / 100 : null, maxPrice90: stats?.max?.[0] != null ? stats.max[0] / 100 : null,
salesRank: stats?.current?.[3] ?? null, salesRank: stats?.current?.[3] ?? null,
salesRankAvg90: stats?.avg?.[3] ?? null, salesRankAvg90: stats?.avg?.[3] ?? null,
salesRankDrops30: product.salesRankDrops30 ?? null, salesRankDrops30,
salesRankDrops90: product.salesRankDrops90 ?? null, salesRankDrops90,
sellerCount: stats?.current?.[11] ?? null, sellerCount: stats?.current?.[11] ?? null,
buyBoxSeller: product.buyBoxSellerId ?? null, buyBoxSeller: product.buyBoxSellerId ?? null,
buyBoxPrice: stats?.current?.[18] != null ? stats.current[18] / 100 : null, buyBoxPrice: stats?.current?.[18] != null ? stats.current[18] / 100 : null,
monthlySold: product.monthlySold ?? null, monthlySold,
categoryTree: product.categoryTree?.map((c: { name: string }) => c.name) ?? [], categoryTree:
product.categoryTree?.map((c: { name: string }) => c.name) ?? [],
}; };
} }
function pickKeepaNumber(...values: unknown[]): number | null {
for (const value of values) {
if (typeof value !== "number" || !Number.isFinite(value)) continue;
// Keepa often uses -1 as "not available".
if (value < 0) continue;
return value;
}
return null;
}
function extractCurrentPrice(csv: number[][] | undefined): number | null { function extractCurrentPrice(csv: number[][] | undefined): number | null {
if (!csv) return null; if (!csv) return null;