diff --git a/lib/cache.ts b/lib/cache.ts new file mode 100644 index 0000000..3e567db --- /dev/null +++ b/lib/cache.ts @@ -0,0 +1,79 @@ +// Simple in-memory cache implementation (Redis simulation for development) +// In production, this would be replaced with actual Redis client + +interface CacheEntry { + value: any + expiry: number +} + +class SimpleCache { + private cache = new Map() + + set(key: string, value: any, ttlSeconds = 300): void { + const expiry = Date.now() + (ttlSeconds * 1000) + this.cache.set(key, { value, expiry }) + } + + get(key: string): T | null { + const entry = this.cache.get(key) + + if (!entry) return null + + if (Date.now() > entry.expiry) { + this.cache.delete(key) + return null + } + + return entry.value as T + } + + del(key: string): void { + this.cache.delete(key) + } + + clear(): void { + this.cache.clear() + } + + // Clean expired entries + cleanup(): void { + const now = Date.now() + Array.from(this.cache.entries()).forEach(([key, entry]) => { + if (now > entry.expiry) { + this.cache.delete(key) + } + }) + } +} + +// Global cache instance +export const cache = new SimpleCache() + +// Cleanup expired entries every 5 minutes +setInterval(() => { + cache.cleanup() +}, 5 * 60 * 1000) + +export function getCacheKey(...parts: (string | number)[]): string { + return parts.join(':') +} + +export async function cacheWrapper( + key: string, + fetcher: () => Promise, + ttl = 300 +): Promise { + // Try to get from cache first + const cached = cache.get(key) + if (cached !== null) { + return cached + } + + // Fetch fresh data + const data = await fetcher() + + // Cache the result + cache.set(key, data, ttl) + + return data +} \ No newline at end of file diff --git a/pages/api/cache/stats.ts b/pages/api/cache/stats.ts new file mode 100644 index 0000000..8497cbc --- /dev/null +++ b/pages/api/cache/stats.ts @@ -0,0 +1,16 @@ +import type { NextApiRequest, NextApiResponse } from "next" +import { cache } from "../../../lib/cache" + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "GET") return res.status(405).json({ error: "Method not allowed" }) + + // Simple cache statistics + const stats = { + cache_size: (cache as any).cache?.size || 0, + cache_cleanup_interval: '5 minutes', + last_cleanup: 'Automatic', + cache_implementation: 'In-memory (development mode)' + } + + res.json(stats) +} \ No newline at end of file diff --git a/pages/api/sources/check.ts b/pages/api/sources/check.ts index 41055e0..9791f74 100644 --- a/pages/api/sources/check.ts +++ b/pages/api/sources/check.ts @@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next' import sqlite3 from 'sqlite3' import path from 'path' import { rateLimit, getRateLimitHeaders } from '../../../lib/rate-limiter' +import { cache, getCacheKey } from '../../../lib/cache' type CheckResponse = { is_problematic: boolean @@ -79,6 +80,14 @@ export default async function handler( return res.status(400).json({ error: 'Invalid URL format' }) } + // Check cache first + const cacheKey = getCacheKey('domain_check', domain) + const cachedResult = cache.get(cacheKey) + + if (cachedResult) { + return res.status(200).json(cachedResult) + } + const dbPath = path.join(process.cwd(), 'database', 'antihoax.db') const db = new sqlite3.Database(dbPath) @@ -99,42 +108,50 @@ export default async function handler( ) }) + let result: CheckResponse + if (sources.length === 0) { - return res.status(200).json({ + result = { is_problematic: false, risk_level: 0, categories: [], message: 'Stránka nie je v našej databáze problematických zdrojov', source_count: 0 - }) - } - - const maxRiskLevel = Math.max(...sources.map(s => s.risk_level)) - const allCategories = sources - .map(s => s.categories) - .filter(Boolean) - .join(',') - .split(',') - .filter(Boolean) - - const uniqueCategories = Array.from(new Set(allCategories)) - - let message = '' - if (maxRiskLevel >= 4) { - message = 'VYSOKÉ RIZIKO: Táto stránka šíri nebezpečné obsahy' - } else if (maxRiskLevel >= 3) { - message = 'STREDNÉ RIZIKO: Táto stránka môže obsahovať problematické informácie' + } } else { - message = 'NÍZKE RIZIKO: Táto stránka je označená ako problematická' + const maxRiskLevel = Math.max(...sources.map(s => s.risk_level)) + const allCategories = sources + .map(s => s.categories) + .filter(Boolean) + .join(',') + .split(',') + .filter(Boolean) + + const uniqueCategories = Array.from(new Set(allCategories)) + + let message = '' + if (maxRiskLevel >= 4) { + message = 'VYSOKÉ RIZIKO: Táto stránka šíri nebezpečné obsahy' + } else if (maxRiskLevel >= 3) { + message = 'STREDNÉ RIZIKO: Táto stránka môže obsahovať problematické informácie' + } else { + message = 'NÍZKE RIZIKO: Táto stránka je označená ako problematická' + } + + result = { + is_problematic: true, + risk_level: maxRiskLevel, + categories: uniqueCategories, + message, + source_count: sources.length + } } - return res.status(200).json({ - is_problematic: true, - risk_level: maxRiskLevel, - categories: uniqueCategories, - message, - source_count: sources.length - }) + // Cache the result (5 minutes for non-problematic, 15 minutes for problematic) + const cacheTtl = result.is_problematic ? 900 : 300 + cache.set(cacheKey, result, cacheTtl) + + return res.status(200).json(result) } catch (error) { console.error('Database error:', error)