docs-v2/cypress/support/link-cache.js

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()
};
};