diff --git a/server/index.js b/server/index.js index 7aa12b6..052114e 100644 --- a/server/index.js +++ b/server/index.js @@ -36,7 +36,13 @@ const PORT = process.env.PORT || 3001; const ILAN_BASE = 'https://www.ilan.gov.tr'; const CACHE_TTL_MS = 10 * 60 * 1000; // 10 dk const LIST_SIZE = 20; // ilan.gov.tr sayfa başına 20 kayıtla sınırlıyor -const MAX_PAGES = 8; // 8 sayfa × 20 = 160 ilan +// ilan.gov.tr sayfa başına 20 kayıt veriyor; havuzun tamamı için çok sayfa gerekir. +// Sayfalar paralel çekilir, ilk yanıt beklemesin diye kalanı arka planda dolar. +const MAX_PAGES = Number(process.env.ILAN_MAX_PAGES || 300); // 300 × 20 = 6000 ilan +const ILK_PARTI = Number(process.env.ILAN_ILK_PARTI || 6); // ilk yanıt için yeterli sayfa +const ES_ZAMANLI = Number(process.env.ILAN_ESZAMANLI || 6); // paralel istek sayısı +const DT_LIMIT = Number(process.env.DT_LIMIT || 5000); +const HISTORY_LIMIT = Number(process.env.HISTORY_LIMIT || 5000); const ILAN_HEADERS = { accept: 'application/json', @@ -257,28 +263,85 @@ function htmlToText(htmlStr) { const cache = { tenders: null, fetchedAt: 0, totalCount: 0, todayCount: 0, detail: new Map() }; +let arkaPlanDoluyor = false; + +// Bir sayfa grubunu paralel çeker +async function ilanSayfalari(baslangic, adet) { + const istekler = []; + for (let i = 0; i < adet; i++) { + const page = baslangic + i; + istekler.push( + ilanSearch({ ats: [3] }, page * LIST_SIZE, LIST_SIZE).catch((e) => { + console.warn(`[ihaleist-api] sayfa ${page} alınamadı: ${e.message}`); + return { ads: [] }; + }) + ); + } + return Promise.all(istekler); +} + +function ilanlariBirlestir(mevcut, yeniAds) { + const seen = new Set(mevcut.map((t) => t.id)); + const eklenen = []; + for (const ad of yeniAds) { + const t = mapAd(ad); + if (seen.has(t.id)) continue; + seen.add(t.id); + eklenen.push(t); + } + return eklenen; +} + +// Kalan sayfaları arka planda çeker; kullanıcı beklemez, havuz zamanla dolar. +async function arkaPlandaTamamla(numFound) { + if (arkaPlanDoluyor) return; + arkaPlanDoluyor = true; + try { + const gerekliSayfa = Math.min(MAX_PAGES, Math.ceil(numFound / LIST_SIZE)); + let page = ILK_PARTI; + while (page < gerekliSayfa) { + const grup = Math.min(ES_ZAMANLI, gerekliSayfa - page); + const sonuclar = await ilanSayfalari(page, grup); + const ads = sonuclar.flatMap((r) => r.ads || []); + if (!ads.length) break; + const eklenen = ilanlariBirlestir(cache.tenders, ads); + if (eklenen.length) cache.tenders = [...cache.tenders, ...eklenen]; + page += grup; + if (page % 60 === 0) { + console.log(`[ihaleist-api] arka plan: ${cache.tenders.length}/${numFound} ilan yüklendi`); + } + await new Promise((r) => setTimeout(r, 120)); // kaynağı yormamak için + } + console.log(`[ihaleist-api] havuz dolduruldu: ${cache.tenders.length}/${numFound} ilan`); + } catch (e) { + console.warn('[ihaleist-api] arka plan doldurma hatası:', e.message); + } finally { + arkaPlanDoluyor = false; + } +} + async function getTenders(force = false) { const fresh = cache.tenders && Date.now() - cache.fetchedAt < CACHE_TTL_MS; if (fresh && !force) return cache; - const allAds = []; - let numFound = 0; - for (let page = 0; page < MAX_PAGES; page++) { - const result = await ilanSearch({ ats: [3] }, page * LIST_SIZE, LIST_SIZE); - const ads = result.ads || []; - numFound = result.numFound || numFound; - if (ads.length === 0) break; - allAds.push(...ads); - if (allAds.length >= numFound) break; - } + // İlk parti: paralel, hızlı — kullanıcı bunu bekler + const ilk = await ilanSayfalari(0, ILK_PARTI); + const numFound = ilk.find((r) => r.numFound)?.numFound || 0; + const ads = ilk.flatMap((r) => r.ads || []); + const seen = new Set(); - cache.tenders = allAds + cache.tenders = ads .filter((ad) => (seen.has(ad.id) ? false : seen.add(ad.id))) .map(mapAd); - cache.totalCount = numFound || allAds.length; + cache.totalCount = numFound || cache.tenders.length; cache.todayCount = cache.tenders.filter((t) => t.isYeni).length; cache.fetchedAt = Date.now(); - console.log(`[ihaleist-api] ${cache.tenders.length} ihale yüklendi (toplam havuz: ${cache.totalCount})`); + console.log(`[ihaleist-api] ${cache.tenders.length} ihale yüklendi (havuz: ${cache.totalCount}) — kalanı arka planda`); + + // Kalanı arka planda doldur (yanıtı bloklamaz) + if (cache.totalCount > cache.tenders.length) { + arkaPlandaTamamla(cache.totalCount); + } return cache; } @@ -517,14 +580,14 @@ const server = http.createServer(async (req, res) => { // Doğrudan temin (EKAP worker) verisini varsa birleştir let dt = { tenders: [], totalCount: 0, available: false }; try { - dt = readDtTenders(300); + dt = readDtTenders(DT_LIMIT); } catch (e) { console.warn('[ihaleist-api] DT okuma hatası:', e.message); } // Geçmiş sonuç ilanları (kazanan firma + bedel + teklif sayısı) let hist = { tenders: [], totalCount: 0, available: false }; try { - hist = readHistory(600); + hist = readHistory(HISTORY_LIMIT); } catch (e) { console.warn('[ihaleist-api] geçmiş okuma hatası:', e.message); } @@ -535,7 +598,9 @@ const server = http.createServer(async (req, res) => { success: true, source: sources.join(' + '), fetchedAt: new Date(c.fetchedAt).toLocaleString('tr-TR'), - totalCount: c.totalCount + dt.totalCount, + // Havuz = canlı ilan + doğrudan temin + sonuç arşivi (yüklü sayısıyla tutarlı olsun) + totalCount: c.totalCount + dt.totalCount + hist.totalCount, + ilanCount: c.totalCount, todayCount: c.todayCount, dtCount: dt.totalCount, historyCount: hist.totalCount,