caching layer implementation for improved performance
This commit is contained in:
79
lib/cache.ts
Normal file
79
lib/cache.ts
Normal 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
16
pages/api/cache/stats.ts
vendored
Normal 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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user