fix: Gitea Traefik routing and connection pool optimization
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
This commit is contained in:
399
resources/js/modules/cache-manager/CacheManager.js
Normal file
399
resources/js/modules/cache-manager/CacheManager.js
Normal file
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* Cache Manager Module
|
||||
*
|
||||
* Provides intelligent caching for API responses and computed values.
|
||||
* Features:
|
||||
* - Memory cache
|
||||
* - IndexedDB cache
|
||||
* - Cache invalidation strategies
|
||||
* - Cache warming
|
||||
* - Cache analytics
|
||||
* - Integration with RequestDeduplicator
|
||||
*/
|
||||
|
||||
import { Logger } from '../../core/logger.js';
|
||||
|
||||
/**
|
||||
* Cache strategies
|
||||
*/
|
||||
export const CacheStrategy = {
|
||||
NO_CACHE: 'no-cache',
|
||||
CACHE_FIRST: 'cache-first',
|
||||
NETWORK_FIRST: 'network-first',
|
||||
STALE_WHILE_REVALIDATE: 'stale-while-revalidate',
|
||||
NETWORK_ONLY: 'network-only',
|
||||
CACHE_ONLY: 'cache-only'
|
||||
};
|
||||
|
||||
/**
|
||||
* CacheManager - Intelligent caching system
|
||||
*/
|
||||
export class CacheManager {
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
defaultStrategy: config.defaultStrategy || CacheStrategy.STALE_WHILE_REVALIDATE,
|
||||
defaultTTL: config.defaultTTL || 3600000, // 1 hour
|
||||
maxMemorySize: config.maxMemorySize || 50, // Max 50 items in memory
|
||||
enableIndexedDB: config.enableIndexedDB ?? true,
|
||||
indexedDBName: config.indexedDBName || 'app-cache',
|
||||
indexedDBVersion: config.indexedDBVersion || 1,
|
||||
enableAnalytics: config.enableAnalytics ?? false,
|
||||
...config
|
||||
};
|
||||
|
||||
this.memoryCache = new Map(); // Map<key, CacheEntry>
|
||||
this.indexedDBCache = null;
|
||||
this.analytics = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
sets: 0,
|
||||
deletes: 0
|
||||
};
|
||||
|
||||
// Initialize
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new CacheManager instance
|
||||
*/
|
||||
static create(config = {}) {
|
||||
return new CacheManager(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize cache manager
|
||||
*/
|
||||
async init() {
|
||||
// Initialize IndexedDB if enabled
|
||||
if (this.config.enableIndexedDB && 'indexedDB' in window) {
|
||||
try {
|
||||
await this.initIndexedDB();
|
||||
} catch (error) {
|
||||
Logger.error('[CacheManager] Failed to initialize IndexedDB', error);
|
||||
this.config.enableIndexedDB = false;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info('[CacheManager] Initialized', {
|
||||
strategy: this.config.defaultStrategy,
|
||||
memoryCache: true,
|
||||
indexedDB: this.config.enableIndexedDB
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize IndexedDB
|
||||
*/
|
||||
async initIndexedDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.config.indexedDBName, this.config.indexedDBVersion);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.indexedDBCache = request.result;
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains('cache')) {
|
||||
db.createObjectStore('cache', { keyPath: 'key' });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from cache
|
||||
*/
|
||||
async get(key, options = {}) {
|
||||
const strategy = options.strategy || this.config.defaultStrategy;
|
||||
const ttl = options.ttl || this.config.defaultTTL;
|
||||
|
||||
// Check memory cache first
|
||||
const memoryEntry = this.memoryCache.get(key);
|
||||
if (memoryEntry && !this.isExpired(memoryEntry, ttl)) {
|
||||
this.analytics.hits++;
|
||||
Logger.debug('[CacheManager] Cache hit (memory)', key);
|
||||
return memoryEntry.value;
|
||||
}
|
||||
|
||||
// Check IndexedDB if enabled
|
||||
if (this.config.enableIndexedDB && this.indexedDBCache) {
|
||||
try {
|
||||
const indexedDBEntry = await this.getFromIndexedDB(key);
|
||||
if (indexedDBEntry && !this.isExpired(indexedDBEntry, ttl)) {
|
||||
// Promote to memory cache
|
||||
this.memoryCache.set(key, indexedDBEntry);
|
||||
this.analytics.hits++;
|
||||
Logger.debug('[CacheManager] Cache hit (IndexedDB)', key);
|
||||
return indexedDBEntry.value;
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[CacheManager] IndexedDB get error', error);
|
||||
}
|
||||
}
|
||||
|
||||
this.analytics.misses++;
|
||||
Logger.debug('[CacheManager] Cache miss', key);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value in cache
|
||||
*/
|
||||
async set(key, value, options = {}) {
|
||||
const ttl = options.ttl || this.config.defaultTTL;
|
||||
const strategy = options.strategy || this.config.defaultStrategy;
|
||||
|
||||
const entry = {
|
||||
key,
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
ttl,
|
||||
strategy
|
||||
};
|
||||
|
||||
// Store in memory cache
|
||||
this.memoryCache.set(key, entry);
|
||||
|
||||
// Limit memory cache size
|
||||
if (this.memoryCache.size > this.config.maxMemorySize) {
|
||||
const firstKey = this.memoryCache.keys().next().value;
|
||||
this.memoryCache.delete(firstKey);
|
||||
}
|
||||
|
||||
// Store in IndexedDB if enabled
|
||||
if (this.config.enableIndexedDB && this.indexedDBCache) {
|
||||
try {
|
||||
await this.setInIndexedDB(entry);
|
||||
} catch (error) {
|
||||
Logger.error('[CacheManager] IndexedDB set error', error);
|
||||
}
|
||||
}
|
||||
|
||||
this.analytics.sets++;
|
||||
Logger.debug('[CacheManager] Cache set', key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or compute value (cache-aside pattern)
|
||||
*/
|
||||
async getOrSet(key, computeFn, options = {}) {
|
||||
const strategy = options.strategy || this.config.defaultStrategy;
|
||||
|
||||
// Try cache first (unless network-only)
|
||||
if (strategy !== CacheStrategy.NETWORK_ONLY) {
|
||||
const cached = await this.get(key, options);
|
||||
if (cached !== null) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute value
|
||||
const value = await computeFn();
|
||||
|
||||
// Store in cache (unless no-cache)
|
||||
if (strategy !== CacheStrategy.NO_CACHE) {
|
||||
await this.set(key, value, options);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete value from cache
|
||||
*/
|
||||
async delete(key) {
|
||||
// Delete from memory
|
||||
this.memoryCache.delete(key);
|
||||
|
||||
// Delete from IndexedDB
|
||||
if (this.config.enableIndexedDB && this.indexedDBCache) {
|
||||
try {
|
||||
await this.deleteFromIndexedDB(key);
|
||||
} catch (error) {
|
||||
Logger.error('[CacheManager] IndexedDB delete error', error);
|
||||
}
|
||||
}
|
||||
|
||||
this.analytics.deletes++;
|
||||
Logger.debug('[CacheManager] Cache delete', key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
*/
|
||||
async clear() {
|
||||
this.memoryCache.clear();
|
||||
|
||||
if (this.config.enableIndexedDB && this.indexedDBCache) {
|
||||
try {
|
||||
await this.clearIndexedDB();
|
||||
} catch (error) {
|
||||
Logger.error('[CacheManager] IndexedDB clear error', error);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info('[CacheManager] Cache cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache by pattern
|
||||
*/
|
||||
async invalidate(pattern) {
|
||||
const keysToDelete = [];
|
||||
|
||||
// Find matching keys in memory
|
||||
for (const key of this.memoryCache.keys()) {
|
||||
if (this.matchesPattern(key, pattern)) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete matching keys
|
||||
for (const key of keysToDelete) {
|
||||
await this.delete(key);
|
||||
}
|
||||
|
||||
Logger.info('[CacheManager] Invalidated cache', { pattern, count: keysToDelete.length });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key matches pattern
|
||||
*/
|
||||
matchesPattern(key, pattern) {
|
||||
if (typeof pattern === 'string') {
|
||||
return key.includes(pattern);
|
||||
}
|
||||
if (pattern instanceof RegExp) {
|
||||
return pattern.test(key);
|
||||
}
|
||||
if (typeof pattern === 'function') {
|
||||
return pattern(key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cache entry is expired
|
||||
*/
|
||||
isExpired(entry, ttl) {
|
||||
if (!entry || !entry.timestamp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const age = Date.now() - entry.timestamp;
|
||||
return age > (entry.ttl || ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get from IndexedDB
|
||||
*/
|
||||
async getFromIndexedDB(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.indexedDBCache.transaction(['cache'], 'readonly');
|
||||
const store = transaction.objectStore('cache');
|
||||
const request = store.get(key);
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set in IndexedDB
|
||||
*/
|
||||
async setInIndexedDB(entry) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.indexedDBCache.transaction(['cache'], 'readwrite');
|
||||
const store = transaction.objectStore('cache');
|
||||
const request = store.put(entry);
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete from IndexedDB
|
||||
*/
|
||||
async deleteFromIndexedDB(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.indexedDBCache.transaction(['cache'], 'readwrite');
|
||||
const store = transaction.objectStore('cache');
|
||||
const request = store.delete(key);
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear IndexedDB
|
||||
*/
|
||||
async clearIndexedDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.indexedDBCache.transaction(['cache'], 'readwrite');
|
||||
const store = transaction.objectStore('cache');
|
||||
const request = store.clear();
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Warm cache (preload values)
|
||||
*/
|
||||
async warm(keys, computeFn) {
|
||||
Logger.info('[CacheManager] Warming cache', { count: keys.length });
|
||||
|
||||
for (const key of keys) {
|
||||
try {
|
||||
await this.getOrSet(key, () => computeFn(key));
|
||||
} catch (error) {
|
||||
Logger.error(`[CacheManager] Failed to warm cache for ${key}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache analytics
|
||||
*/
|
||||
getAnalytics() {
|
||||
const total = this.analytics.hits + this.analytics.misses;
|
||||
const hitRate = total > 0 ? (this.analytics.hits / total) * 100 : 0;
|
||||
|
||||
return {
|
||||
...this.analytics,
|
||||
total,
|
||||
hitRate: Math.round(hitRate * 100) / 100,
|
||||
memorySize: this.memoryCache.size,
|
||||
memoryMaxSize: this.config.maxMemorySize
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset analytics
|
||||
*/
|
||||
resetAnalytics() {
|
||||
this.analytics = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
sets: 0,
|
||||
deletes: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy cache manager
|
||||
*/
|
||||
destroy() {
|
||||
this.memoryCache.clear();
|
||||
this.indexedDBCache = null;
|
||||
Logger.info('[CacheManager] Destroyed');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user