yenilemede ihale detay onbelleğini tazele
Quality gates / test-build (push) Canceled after 0s
Quality gates / container (push) Canceled after 0s

This commit is contained in:
2026-09-06 21:23:45 +03:00
parent fd5db7544e
commit 4eb90615e8
4 changed files with 68 additions and 4 deletions
+6
View File
@@ -2390,3 +2390,9 @@ Coolify uygulamasındaki sağlık kontrolü `/api/ready` ve port 80 olarak etkin
### 2026-09-06 · Canlı probe doküman tarayıcısı doğrulaması
`qa:live` drift probe'una `/api/documents/config` kontrolü eklendi. Böylece üretimde EKAP dokümanları için gereken yerleşik Playwright/Chromium yeteneği `tarayiciVar:true` ile canlı ve salt okunur doğrulanıyor; kontrol başarısızsa servis sağlıklı görünse bile belge fallback'i kabul edilmiyor. 6 Eylül 2026 canlı çalıştırmasında home, health, readiness, güvenlik başlıkları ve doküman tarayıcısı kontrolleri geçti.
### 2026-09-06 · Resmî ihale detay önbelleğinin yenilenmesi
İlan listesi yenilendiğinde daha önce alınmış detay metni süresiz kullanılmıyor. Detay önbelleği 10 dakikalık yaşam süresi ve 500 kayıt sınırıyla çalışıyor; kullanıcı `refresh=1` istediğinde liste kaynağı başarıyla alındıktan sonra ayrıntılar temizleniyor. Böylece zeyilname veya resmî tarih değişikliği eski detayın kartla birleştirilmesine yol açmıyor. Liste henüz hazır değilken detay isteği de artık hata yerine kaynak ayrıntısını alabiliyor.
Doğrulama: `tests/detail-cache.test.mjs` önbellek süresi, açık yenileme ve sınırdan taşan kaydın atılmasını doğruluyor; `npm test` 304/304 başarılı. Üretim rollout ve canlı beşli probe bu değişiklikten sonra ayrıca yapılmalı.
+30
View File
@@ -0,0 +1,30 @@
/**
* Resmî ihale detayları için süreli, sınırlı bellek önbelleği.
*
* Liste yenilense bile detay metni ayrı bir kaynaktan geldiği için sınırsız
* süre saklanamaz; zeyilname veya tarih değişikliği eski ayrıntıyı geçersiz
* kılmalıdır. Saat bağımlılığını test edebilmek için now parametresi açıktır.
*/
export function createDetailCache({ maxEntries = 500, ttlMs = 10 * 60 * 1000 } = {}) {
if (!Number.isInteger(maxEntries) || maxEntries < 1) throw new Error('Detay önbelleği sınırı geçersiz.');
if (!Number.isFinite(ttlMs) || ttlMs <= 0) throw new Error('Detay önbelleği süresi geçersiz.');
const entries = new Map();
return {
get(key, now = Date.now()) {
const entry = entries.get(key);
if (!entry) return undefined;
if (now - entry.fetchedAt >= ttlMs) {
entries.delete(key);
return undefined;
}
return entry.value;
},
set(key, value, now = Date.now()) {
entries.delete(key);
entries.set(key, { value, fetchedAt: now });
while (entries.size > maxEntries) entries.delete(entries.keys().next().value);
},
clear() { entries.clear(); },
get size() { return entries.size; },
};
}
+8 -4
View File
@@ -64,6 +64,7 @@ import { belgeyeSor, teklifDenetle, teklifTaslak, ihaleMetni, gereklilikMetni }
import { subscribe, list as listSubs, remove as removeSub, deliveryQueue, resolveDelivery, available as notifyAvailable } from './notify-store.mjs';
import { bildirimleriIsle } from './notifier.mjs';
import { scanSite } from './site-scan.mjs';
import { createDetailCache } from './detail-cache.mjs';
// ilan.gov.tr sunucusu ara sertifikayı (GeoTrust TLS RSA CA G1) göndermiyor;
// zinciri tamamlamak için PEM'i yerelden ekliyoruz.
@@ -336,7 +337,7 @@ function htmlToText(htmlStr) {
.trim();
}
const cache = { tenders: null, fetchedAt: 0, totalCount: 0, todayCount: 0, detail: new Map() };
const cache = { tenders: null, fetchedAt: 0, totalCount: 0, todayCount: 0, detail: createDetailCache() };
let arkaPlanDoluyor = false;
@@ -596,6 +597,9 @@ async function getTenders(force = false) {
if (fresh && !force) return cache;
const ilk = await ilanSayfalari(0, ILK_PARTI);
// Listeyi zorla yenileyen kullanıcı güncel kaynak ayrıntısını da görmeli;
// eski detay metni yeni kartla birleştirilmemeli.
if (force) cache.detail.clear();
const numFound = ilk.find((r) => r.numFound)?.numFound || 0;
const ads = ilk.flatMap((r) => r.ads || []);
@@ -624,17 +628,17 @@ async function getTenders(force = false) {
}
async function getDetail(ilanId) {
if (cache.detail.has(ilanId)) return cache.detail.get(ilanId);
const cached = cache.detail.get(ilanId);
if (cached) return cached;
const raw = await ilanDetail(ilanId);
const text = htmlToText(raw.content || '');
const detail = { ...announcementFields(text), adNo: raw.adNo || null };
const sourceTender=cache.tenders.find(t=>t.id===`ilan-${ilanId}`);
const sourceTender=(cache.tenders || []).find(t=>t.id===`ilan-${ilanId}`);
if(sourceTender) {
try { observeTenders([{...sourceTender,...detail,id:`ilan-${ilanId}:detail`}]); }
catch { detail.sourceWarning='İlan alındı ancak kaynak geçmişi kaydedilemedi.'; }
}
cache.detail.set(ilanId, detail);
if (cache.detail.size > 500) cache.detail.delete(cache.detail.keys().next().value);
return detail;
}
+24
View File
@@ -0,0 +1,24 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createDetailCache } from '../server/detail-cache.mjs';
test('detail cache expires stale source data and supports explicit refresh', () => {
const cache = createDetailCache({ maxEntries: 2, ttlMs: 100 });
cache.set('2026/1', { tarih: '10.09.2026' }, 1000);
assert.deepEqual(cache.get('2026/1', 1099), { tarih: '10.09.2026' });
assert.equal(cache.get('2026/1', 1100), undefined);
cache.set('2026/1', { tarih: '16.09.2026' }, 1200);
assert.deepEqual(cache.get('2026/1', 1200), { tarih: '16.09.2026' });
cache.clear();
assert.equal(cache.size, 0);
});
test('detail cache evicts the oldest entry at its bounded limit', () => {
const cache = createDetailCache({ maxEntries: 2, ttlMs: 1000 });
cache.set('a', 1, 0);
cache.set('b', 2, 0);
cache.set('c', 3, 0);
assert.equal(cache.get('a', 1), undefined);
assert.equal(cache.get('b', 1), 2);
assert.equal(cache.get('c', 1), 3);
});