diff --git a/backend/services/newsService.js b/backend/services/newsService.js index 36ad4f6..b041458 100644 --- a/backend/services/newsService.js +++ b/backend/services/newsService.js @@ -6,6 +6,10 @@ const NEWS_BASE = 'https://newsdata.io/api/1/news'; +// Cache für News-Ergebnisse (pro Sprache/Kategorie) +const newsCache = new Map(); +const CACHE_TTL = 5 * 60 * 1000; // 5 Minuten Cache + /** * @param {object} options * @param {number} options.counter - 0 = erste Seite, 1 = zweite, … (für Pagination/nextPage) @@ -13,7 +17,7 @@ const NEWS_BASE = 'https://newsdata.io/api/1/news'; * @param {string} [options.category] - z. B. top, technology * @returns {Promise<{ results: Array, nextPage: string|null }>} */ -async function fetchNewsPage({ counter, language = 'de', category = 'top', nextPageToken = null }) { +async function fetchNewsPage({ language = 'de', category = 'top', nextPageToken = null }) { const apiKey = process.env.NEWSDATA_IO_API_KEY; if (!apiKey || !apiKey.trim()) { throw new Error('NEWSDATA_IO_API_KEY is not set in .env'); @@ -37,9 +41,64 @@ async function fetchNewsPage({ counter, language = 'de', category = 'top', nextP }; } +/** + * Holt gecachte Artikel oder lädt sie von der API + */ +async function getCachedNews({ language = 'de', category = 'top', minArticles = 10 }) { + const cacheKey = `${language}:${category}`; + const cached = newsCache.get(cacheKey); + + // Cache gültig? + if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) { + // Wenn wir mehr Artikel brauchen, erweitern + if (cached.articles.length >= minArticles) { + return cached.articles; + } + } + + // Neue Daten laden + const collected = cached?.articles || []; + let nextPageToken = cached?.nextPage || null; + + while (collected.length < minArticles) { + try { + const page = await fetchNewsPage({ + language, + category, + nextPageToken: nextPageToken || undefined + }); + const items = page.results ?? []; + + // Duplikate vermeiden (nach title) + const existingTitles = new Set(collected.map(a => a.title)); + for (const item of items) { + if (!existingTitles.has(item.title)) { + collected.push(item); + existingTitles.add(item.title); + } + } + + nextPageToken = page.nextPage ?? null; + if (items.length === 0 || !nextPageToken) break; + } catch (error) { + console.error('News fetch error:', error); + break; + } + } + + // Cache aktualisieren + newsCache.set(cacheKey, { + articles: collected, + nextPage: nextPageToken, + timestamp: Date.now() + }); + + return collected; +} + /** * Liefert den N-ten Artikel (counter = 0, 1, 2, …) für das N-te News-Widget. - * Lädt ggf. mehrere Seiten, bis genug Artikel vorhanden sind; jede Widget-Instanz bekommt einen anderen Artikel. + * Nutzt Cache, damit mehrere Widgets unterschiedliche Artikel bekommen. * * @param {object} options * @param {number} options.counter - Index des Artikels (0 = erster, 1 = zweiter, …) @@ -49,22 +108,15 @@ async function fetchNewsPage({ counter, language = 'de', category = 'top', nextP */ async function getNews({ counter = 0, language = 'de', category = 'top' }) { const neededIndex = Math.max(0, counter); - const collected = []; - let nextPageToken = null; + + // Mindestens so viele Artikel laden wie benötigt + const articles = await getCachedNews({ + language, + category, + minArticles: neededIndex + 1 + }); - while (collected.length <= neededIndex) { - const page = await fetchNewsPage({ - language, - category, - nextPageToken: nextPageToken || undefined - }); - const items = page.results ?? []; - collected.push(...items); - nextPageToken = page.nextPage ?? null; - if (items.length === 0 || !nextPageToken) break; - } - - const single = collected[neededIndex] ? [collected[neededIndex]] : []; + const single = articles[neededIndex] ? [articles[neededIndex]] : []; return { results: single, nextPage: null }; }