215 lines
5.4 KiB
JavaScript
215 lines
5.4 KiB
JavaScript
/**
|
|
* Link Cache Manager for Cypress Tests
|
|
* Manages caching of link validation results at the URL level
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import crypto from 'crypto';
|
|
|
|
const CACHE_VERSION = 'v2';
|
|
const CACHE_KEY_PREFIX = 'link-validation';
|
|
const LOCAL_CACHE_DIR = path.join(process.cwd(), '.cache', 'link-validation');
|
|
|
|
/**
|
|
* Cache manager for individual link validation results
|
|
*/
|
|
export class LinkCacheManager {
|
|
constructor(options = {}) {
|
|
this.localCacheDir = options.localCacheDir || LOCAL_CACHE_DIR;
|
|
|
|
// Configurable cache TTL - default 30 days
|
|
this.cacheTTLDays =
|
|
options.cacheTTLDays || parseInt(process.env.LINK_CACHE_TTL_DAYS) || 30;
|
|
this.maxAge = this.cacheTTLDays * 24 * 60 * 60 * 1000;
|
|
|
|
this.ensureLocalCacheDir();
|
|
|
|
// Track cache statistics
|
|
this.stats = {
|
|
hits: 0,
|
|
misses: 0,
|
|
stores: 0,
|
|
cleanups: 0
|
|
};
|
|
}
|
|
|
|
ensureLocalCacheDir() {
|
|
if (!fs.existsSync(this.localCacheDir)) {
|
|
fs.mkdirSync(this.localCacheDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate cache key for a URL
|
|
* @param {string} url - The URL to cache
|
|
* @returns {string} Cache key
|
|
*/
|
|
generateCacheKey(url) {
|
|
const urlHash = crypto
|
|
.createHash('sha256')
|
|
.update(url)
|
|
.digest('hex')
|
|
.substring(0, 16);
|
|
return `${CACHE_KEY_PREFIX}-${CACHE_VERSION}-${urlHash}`;
|
|
}
|
|
|
|
/**
|
|
* Get cache file path for a URL
|
|
* @param {string} url - The URL
|
|
* @returns {string} File path
|
|
*/
|
|
getCacheFilePath(url) {
|
|
const cacheKey = this.generateCacheKey(url);
|
|
return path.join(this.localCacheDir, `${cacheKey}.json`);
|
|
}
|
|
|
|
/**
|
|
* Check if a URL's validation result is cached
|
|
* @param {string} url - The URL to check
|
|
* @returns {Object|null} Cached result or null
|
|
*/
|
|
get(url) {
|
|
const cacheFile = this.getCacheFilePath(url);
|
|
|
|
if (!fs.existsSync(cacheFile)) {
|
|
this.stats.misses++;
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const content = fs.readFileSync(cacheFile, 'utf8');
|
|
const cached = JSON.parse(content);
|
|
|
|
// TTL check
|
|
const age = Date.now() - new Date(cached.cachedAt).getTime();
|
|
|
|
if (age > this.maxAge) {
|
|
fs.unlinkSync(cacheFile);
|
|
this.stats.misses++;
|
|
this.stats.cleanups++;
|
|
return null;
|
|
}
|
|
|
|
this.stats.hits++;
|
|
return cached;
|
|
} catch (error) {
|
|
// Clean up corrupted cache
|
|
try {
|
|
fs.unlinkSync(cacheFile);
|
|
this.stats.cleanups++;
|
|
} catch (cleanupError) {
|
|
// Ignoring cleanup errors as they are non-critical, but logging for visibility
|
|
console.warn(`Failed to clean up corrupted cache file: ${cleanupError.message}`);
|
|
}
|
|
this.stats.misses++;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store validation result for a URL
|
|
* @param {string} url - The URL
|
|
* @param {Object} result - Validation result
|
|
* @returns {boolean} True if successfully cached, false otherwise
|
|
*/
|
|
set(url, result) {
|
|
const cacheFile = this.getCacheFilePath(url);
|
|
|
|
const cacheData = {
|
|
url,
|
|
result,
|
|
cachedAt: new Date().toISOString(),
|
|
ttl: new Date(Date.now() + this.maxAge).toISOString()
|
|
};
|
|
|
|
try {
|
|
fs.writeFileSync(cacheFile, JSON.stringify(cacheData, null, 2));
|
|
this.stats.stores++;
|
|
return true;
|
|
} catch (error) {
|
|
console.warn(`Failed to cache validation result for ${url}: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a URL is cached and valid
|
|
* @param {string} url - The URL to check
|
|
* @returns {boolean} True if cached and valid
|
|
*/
|
|
isCached(url) {
|
|
return this.get(url) !== null;
|
|
}
|
|
|
|
/**
|
|
* Get cache statistics
|
|
* @returns {Object} Cache statistics
|
|
*/
|
|
getStats() {
|
|
const total = this.stats.hits + this.stats.misses;
|
|
const hitRate = total > 0 ? (this.stats.hits / total * 100).toFixed(1) : 0;
|
|
|
|
return {
|
|
...this.stats,
|
|
total,
|
|
hitRate: `${hitRate}%`
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Clean up expired cache entries
|
|
* @returns {number} Number of entries cleaned up
|
|
*/
|
|
cleanup() {
|
|
let cleaned = 0;
|
|
|
|
try {
|
|
const files = fs.readdirSync(this.localCacheDir);
|
|
const cacheFiles = files.filter(file =>
|
|
file.startsWith(CACHE_KEY_PREFIX) && file.endsWith('.json')
|
|
);
|
|
|
|
for (const file of cacheFiles) {
|
|
const filePath = path.join(this.localCacheDir, file);
|
|
|
|
try {
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const cached = JSON.parse(content);
|
|
|
|
const age = Date.now() - new Date(cached.cachedAt).getTime();
|
|
|
|
if (age > this.maxAge) {
|
|
fs.unlinkSync(filePath);
|
|
cleaned++;
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Failed to process cache file "${filePath}": ${error.message}`);
|
|
// Remove corrupted files
|
|
fs.unlinkSync(filePath);
|
|
cleaned++;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Cache cleanup failed: ${error.message}`);
|
|
}
|
|
|
|
this.stats.cleanups += cleaned;
|
|
return cleaned;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cypress task helper to integrate cache with Cypress tasks
|
|
*/
|
|
export const createCypressCacheTasks = (options = {}) => {
|
|
const cache = new LinkCacheManager(options);
|
|
|
|
return {
|
|
getLinkCache: (url) => cache.get(url),
|
|
setLinkCache: ({ url, result }) => cache.set(url, result),
|
|
isLinkCached: (url) => cache.isCached(url),
|
|
getCacheStats: () => cache.getStats(),
|
|
cleanupCache: () => cache.cleanup()
|
|
};
|
|
}; |