caching layer implementation for improved performance

This commit is contained in:
2025-05-12 16:45:28 +02:00
parent 8022fceff4
commit e1c6a35325
3 changed files with 139 additions and 27 deletions

79
lib/cache.ts Normal file
View File

@@ -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<string, CacheEntry>()
set(key: string, value: any, ttlSeconds = 300): void {
const expiry = Date.now() + (ttlSeconds * 1000)
this.cache.set(key, { value, expiry })
}
get<T>(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<T>(
key: string,
fetcher: () => Promise<T>,
ttl = 300
): Promise<T> {
// Try to get from cache first
const cached = cache.get<T>(key)
if (cached !== null) {
return cached
}
// Fetch fresh data
const data = await fetcher()
// Cache the result
cache.set(key, data, ttl)
return data
}

16
pages/api/cache/stats.ts vendored Normal file
View File

@@ -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)
}

View File

@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'
import sqlite3 from 'sqlite3' import sqlite3 from 'sqlite3'
import path from 'path' import path from 'path'
import { rateLimit, getRateLimitHeaders } from '../../../lib/rate-limiter' import { rateLimit, getRateLimitHeaders } from '../../../lib/rate-limiter'
import { cache, getCacheKey } from '../../../lib/cache'
type CheckResponse = { type CheckResponse = {
is_problematic: boolean is_problematic: boolean
@@ -79,6 +80,14 @@ export default async function handler(
return res.status(400).json({ error: 'Invalid URL format' }) return res.status(400).json({ error: 'Invalid URL format' })
} }
// Check cache first
const cacheKey = getCacheKey('domain_check', domain)
const cachedResult = cache.get<CheckResponse>(cacheKey)
if (cachedResult) {
return res.status(200).json(cachedResult)
}
const dbPath = path.join(process.cwd(), 'database', 'antihoax.db') const dbPath = path.join(process.cwd(), 'database', 'antihoax.db')
const db = new sqlite3.Database(dbPath) const db = new sqlite3.Database(dbPath)
@@ -99,42 +108,50 @@ export default async function handler(
) )
}) })
let result: CheckResponse
if (sources.length === 0) { if (sources.length === 0) {
return res.status(200).json({ result = {
is_problematic: false, is_problematic: false,
risk_level: 0, risk_level: 0,
categories: [], categories: [],
message: 'Stránka nie je v našej databáze problematických zdrojov', message: 'Stránka nie je v našej databáze problematických zdrojov',
source_count: 0 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 { } 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({ // Cache the result (5 minutes for non-problematic, 15 minutes for problematic)
is_problematic: true, const cacheTtl = result.is_problematic ? 900 : 300
risk_level: maxRiskLevel, cache.set(cacheKey, result, cacheTtl)
categories: uniqueCategories,
message, return res.status(200).json(result)
source_count: sources.length
})
} catch (error) { } catch (error) {
console.error('Database error:', error) console.error('Database error:', error)