fortify2-js
Version:
MOST POWERFUL JavaScript Security Library! Military-grade cryptography + 19 enhanced object methods + quantum-resistant algorithms + perfect TypeScript support. More powerful than Lodash with built-in security.
844 lines (840 loc) • 30 kB
JavaScript
'use strict';
var CachePlugin = require('../core/CachePlugin.js');
var PluginTypes = require('../types/PluginTypes.js');
/**
* Smart Cache Plugin
*
* Intelligent caching plugin with <0.5ms execution overhead
* leveraging FortifyJS cache systems for optimal performance.
*/
/**
* Smart Cache Plugin for intelligent request caching
*/
class SmartCachePlugin extends CachePlugin.CachePlugin {
constructor() {
super(...arguments);
this.id = "nehonix.ftfy.cache";
this.name = "Smart Cache Plugin";
this.version = "1.0.0";
this.priority = PluginTypes.PluginPriority.HIGH;
// Cache configuration
this.cacheStrategy = "hybrid";
this.compressionEnabled = true;
this.encryptionEnabled = true; // Keep encryption for security
// Smart caching rules
this.cachingRules = new Map();
// Cache analytics
this.cacheAnalytics = {
totalRequests: 0,
cacheableRequests: 0,
cacheHits: 0,
cacheMisses: 0,
cacheSkips: 0,
averageHitTime: 0,
averageMissTime: 0,
compressionSavings: 0,
};
// Dynamic TTL adjustment based on request patterns
this.requestPatterns = new Map();
// Background prefetching queue and worker
this.prefetchQueue = [];
this.prefetchWorkerActive = false;
this.prefetchStats = {
totalPrefetched: 0,
successfulPrefetches: 0,
failedPrefetches: 0,
averagePrefetchTime: 0,
};
}
/**
* Initialize smart cache plugin
*/
async initializeCachePlugin(context) {
// Setup default caching rules
this.setupDefaultCachingRules();
// Configure custom rules from settings
if (context.config.customSettings.cachingRules) {
this.configureCachingRules(context.config.customSettings.cachingRules);
}
// Setup cache analytics cleanup
this.setupAnalyticsCleanup();
// Setup dynamic TTL adjustment
this.setupDynamicTTLAdjustment();
context.logger.info("Smart Cache Plugin initialized with intelligent caching rules");
}
/**
* Check if request should be cached (plugin-specific logic)
*/
shouldCacheRequest(context) {
const { req } = context;
// Apply smart caching rules
for (const [ruleName, rule] of this.cachingRules.entries()) {
if (rule.enabled && rule.pattern.test(req.path)) {
// Check additional conditions
if (this.shouldApplyRule(context, rule)) {
return true;
}
}
}
// Fallback to intelligent heuristics
return this.applyIntelligentCaching(context);
}
/**
* Get custom cache key components
*/
getCustomKeyComponents(context) {
const { req } = context;
const components = [];
// Add user-specific components for personalized content
if (context.security.isAuthenticated) {
components.push(`user:${context.security.userId}`);
// Add role-based caching
if (context.security.roles.length > 0) {
components.push(`roles:${context.security.roles.sort().join(",")}`);
}
}
// Add device type for responsive caching
const userAgent = req.headers["user-agent"];
if (userAgent) {
const deviceType = this.detectDeviceType(userAgent);
components.push(`device:${deviceType}`);
}
// Add language for i18n caching
const acceptLanguage = req.headers["accept-language"];
if (acceptLanguage) {
const primaryLanguage = acceptLanguage.split(",")[0].split("-")[0];
components.push(`lang:${primaryLanguage}`);
}
// Add API version for versioned APIs
const apiVersion = req.headers["api-version"] || req.query.version;
if (apiVersion) {
components.push(`version:${apiVersion}`);
}
return components;
}
/**
* Get custom TTL for request
*/
getCustomTTL(context) {
const { req } = context;
const route = this.normalizeRoute(req.path);
// Check if we have pattern data for dynamic TTL
const pattern = this.requestPatterns.get(route);
if (pattern) {
return this.calculateDynamicTTL(pattern);
}
// Apply rule-based TTL
for (const [ruleName, rule] of this.cachingRules.entries()) {
if (rule.enabled && rule.pattern.test(req.path)) {
return rule.ttl;
}
}
// Default TTL based on content type
return this.getDefaultTTLByContentType(req.path);
}
/**
* Handle custom cache operations
*/
async handleCustomCacheOperation(context, operation) {
switch (operation) {
case "analyze":
return await this.analyzeCachePerformance(context);
case "optimize":
return await this.optimizeCacheStrategy(context);
case "prefetch":
return await this.prefetchRelatedContent(context);
default:
return { operation, supported: false };
}
}
/**
* Precompile cache operations
*/
async precompileCacheOperations() {
// Pre-warm route normalization
this.normalizeRoute("/api/users/123");
// Pre-warm device detection
this.detectDeviceType("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
// Pre-warm TTL calculation
this.calculateDynamicTTL({
frequency: 10,
lastAccess: Date.now(),
averageResponseTime: 100,
volatility: 0.1,
});
}
// ===== SMART CACHING LOGIC =====
/**
* Setup default caching rules
*/
setupDefaultCachingRules() {
// Static assets - long TTL
this.cachingRules.set("static", {
pattern: /\.(css|js|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$/,
ttl: 86400000, // 24 hours
enabled: true,
compression: true,
tags: ["static"],
});
// API responses - medium TTL
this.cachingRules.set("api", {
pattern: /^\/api\/(?!auth|admin)/,
ttl: 300000, // 5 minutes
enabled: true,
compression: true,
tags: ["api"],
});
// Public pages - short TTL
this.cachingRules.set("public", {
pattern: /^\/(?!admin|dashboard|profile)/,
ttl: 60000, // 1 minute
enabled: true,
compression: true,
tags: ["public"],
});
// User-specific content - very short TTL
this.cachingRules.set("user", {
pattern: /^\/(profile|dashboard|settings)/,
ttl: 30000, // 30 seconds
enabled: true,
compression: false,
tags: ["user"],
});
}
/**
* Configure custom caching rules
*/
configureCachingRules(rules) {
for (const rule of rules) {
this.cachingRules.set(rule.name, {
pattern: new RegExp(rule.pattern),
ttl: rule.ttl || 300000,
enabled: rule.enabled !== false,
compression: rule.compression !== false,
tags: rule.tags || [],
});
}
}
/**
* Check if caching rule should be applied
*/
shouldApplyRule(context, rule) {
const { req } = context;
// Don't cache authenticated requests for public rules
if (rule.tags.includes("public") && context.security.isAuthenticated) {
return false;
}
// Don't cache if request has cache-control: no-cache
const cacheControl = req.headers["cache-control"];
if (cacheControl && cacheControl.includes("no-cache")) {
return false;
}
// Don't cache if request has pragma: no-cache
const pragma = req.headers.pragma;
if (pragma && pragma.includes("no-cache")) {
return false;
}
return true;
}
/**
* Apply intelligent caching heuristics
*/
applyIntelligentCaching(context) {
const { req } = context;
// Analyze request characteristics
const hasQueryParams = Object.keys(req.query).length > 0;
const hasBody = req.body && Object.keys(req.body).length > 0;
const isIdempotent = ["GET", "HEAD", "OPTIONS"].includes(req.method);
// Don't cache non-idempotent requests
if (!isIdempotent) {
return false;
}
// Don't cache requests with complex query parameters
if (hasQueryParams && this.hasComplexQueryParams(req.query)) {
return false;
}
// Cache simple GET requests
if (req.method === "GET" && !hasBody) {
return true;
}
return false;
}
/**
* Calculate dynamic TTL based on request patterns
*/
calculateDynamicTTL(pattern) {
const baseTime = 300000; // 5 minutes base
// Adjust based on frequency (more frequent = longer cache)
const frequencyMultiplier = Math.min(pattern.frequency / 10, 2);
// Adjust based on volatility (more volatile = shorter cache)
const volatilityMultiplier = Math.max(1 - pattern.volatility, 0.1);
// Adjust based on response time (slower = longer cache)
const responseTimeMultiplier = Math.min(pattern.averageResponseTime / 100, 3);
return Math.round(baseTime *
frequencyMultiplier *
volatilityMultiplier *
responseTimeMultiplier);
}
/**
* Get default TTL by content type
*/
getDefaultTTLByContentType(path) {
if (path.match(/\.(css|js)$/)) {
return 3600000; // 1 hour for CSS/JS
}
if (path.match(/\.(png|jpg|jpeg|gif|svg|ico)$/)) {
return 86400000; // 24 hours for images
}
if (path.startsWith("/api/")) {
return 300000; // 5 minutes for API
}
return 60000; // 1 minute default
}
// ===== ANALYTICS AND OPTIMIZATION =====
/**
* Analyze cache performance
*/
async analyzeCachePerformance(context) {
const hitRate = this.cacheAnalytics.totalRequests > 0
? (this.cacheAnalytics.cacheHits /
this.cacheAnalytics.totalRequests) *
100
: 0;
const cacheableRate = this.cacheAnalytics.totalRequests > 0
? (this.cacheAnalytics.cacheableRequests /
this.cacheAnalytics.totalRequests) *
100
: 0;
return {
hitRate: Math.round(hitRate * 100) / 100,
cacheableRate: Math.round(cacheableRate * 100) / 100,
totalRequests: this.cacheAnalytics.totalRequests,
cacheHits: this.cacheAnalytics.cacheHits,
cacheMisses: this.cacheAnalytics.cacheMisses,
averageHitTime: this.cacheAnalytics.averageHitTime,
averageMissTime: this.cacheAnalytics.averageMissTime,
compressionSavings: this.cacheAnalytics.compressionSavings,
topPatterns: this.getTopRequestPatterns(),
};
}
/**
* Optimize cache strategy
*/
async optimizeCacheStrategy(context) {
const optimizations = [];
// Analyze hit rates by rule
for (const [ruleName, rule] of this.cachingRules.entries()) {
// Suggest optimizations based on performance
if (rule.enabled) {
optimizations.push(`Rule '${ruleName}' is active`);
}
}
// Suggest TTL adjustments
const ttlSuggestions = this.suggestTTLOptimizations();
optimizations.push(...ttlSuggestions);
return {
optimizations,
suggestions: this.generateOptimizationSuggestions(),
};
}
/**
* Prefetch related content
*/
async prefetchRelatedContent(context) {
const { req } = context;
const relatedUrls = this.identifyRelatedContent(req.path);
// Queue related URLs for background prefetching
const queuedUrls = [];
for (const url of relatedUrls) {
const priority = this.calculatePrefetchPriority(url, context);
// Only queue high-priority URLs to avoid overwhelming the system
if (priority > 0.5) {
this.queueForPrefetch(url, priority, context);
queuedUrls.push(url);
}
}
// Start background prefetch worker if not already active
if (!this.prefetchWorkerActive && this.prefetchQueue.length > 0) {
this.startPrefetchWorker();
}
return {
prefetched: queuedUrls.length,
urls: queuedUrls,
queueSize: this.prefetchQueue.length,
};
}
/**
* Queue a URL for background prefetching
*/
queueForPrefetch(url, priority, context) {
// Avoid duplicate entries
const existingIndex = this.prefetchQueue.findIndex((item) => item.url === url);
if (existingIndex >= 0) {
// Update priority if higher
if (this.prefetchQueue[existingIndex].priority < priority) {
this.prefetchQueue[existingIndex].priority = priority;
this.prefetchQueue[existingIndex].timestamp = Date.now();
}
return;
}
// Add to queue
this.prefetchQueue.push({
url,
priority,
timestamp: Date.now(),
context: {
method: context.req.method,
headers: context.req.headers,
baseUrl: `${context.req.protocol}://${context.req.get("host")}`,
},
});
// Sort by priority (highest first)
this.prefetchQueue.sort((a, b) => b.priority - a.priority);
// Limit queue size to prevent memory issues
if (this.prefetchQueue.length > 100) {
this.prefetchQueue = this.prefetchQueue.slice(0, 100);
}
}
/**
* Calculate prefetch priority for a URL
*/
calculatePrefetchPriority(url, context) {
let priority = 0.3; // Base priority
// Check request patterns
const pattern = this.requestPatterns.get(url);
if (pattern) {
// Higher frequency = higher priority
priority += Math.min(pattern.frequency / 100, 0.4);
// Recent access = higher priority
const timeSinceAccess = Date.now() - pattern.lastAccess;
if (timeSinceAccess < 300000) {
// 5 minutes
priority += 0.2;
}
// Lower volatility = higher priority (more stable content)
priority += (1 - pattern.volatility) * 0.1;
}
// Check if URL matches high-priority patterns
if (url.includes("/api/") || url.includes("/static/")) {
priority += 0.2;
}
// Check if it's a common resource type
if (url.match(/\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2)$/)) {
priority += 0.3;
}
return Math.min(priority, 1.0);
}
/**
* Start the background prefetch worker
*/
async startPrefetchWorker() {
if (this.prefetchWorkerActive)
return;
this.prefetchWorkerActive = true;
try {
while (this.prefetchQueue.length > 0) {
const item = this.prefetchQueue.shift();
if (!item)
break;
// Skip items that are too old (older than 5 minutes)
if (Date.now() - item.timestamp > 300000) {
continue;
}
await this.performPrefetch(item);
// Small delay to prevent overwhelming the server
await new Promise((resolve) => setTimeout(resolve, 10));
}
}
catch (error) {
console.error("Prefetch worker error:", error);
}
finally {
this.prefetchWorkerActive = false;
}
}
/**
* Perform actual prefetch operation
*/
async performPrefetch(item) {
const startTime = Date.now();
try {
// Perform actual HTTP request for prefetching to warm up the cache
const response = await this.performActualPrefetchRequest(item.url, item.context);
if (response.success) {
this.prefetchStats.successfulPrefetches++;
// Update request patterns
this.updateRequestPattern(item.url, Date.now() - startTime);
}
else {
this.prefetchStats.failedPrefetches++;
}
this.prefetchStats.totalPrefetched++;
// Update average prefetch time
const prefetchTime = Date.now() - startTime;
this.prefetchStats.averagePrefetchTime =
(this.prefetchStats.averagePrefetchTime *
(this.prefetchStats.totalPrefetched - 1) +
prefetchTime) /
this.prefetchStats.totalPrefetched;
}
catch (error) {
this.prefetchStats.failedPrefetches++;
this.prefetchStats.totalPrefetched++;
console.error(`Prefetch failed for ${item.url}:`, error);
}
}
/**
* Perform actual prefetch request with real HTTP call
*/
async performActualPrefetchRequest(url, context) {
try {
// Construct full URL if relative
let fullUrl = url;
if (context?.baseUrl && !url.startsWith("http")) {
fullUrl = `${context.baseUrl}${url.startsWith("/") ? url : "/" + url}`;
}
// Prepare request options
const requestOptions = {
method: context?.method || "GET",
headers: {
"User-Agent": "FortifyJS-SmartCache/1.0",
Accept: "*/*",
"Cache-Control": "no-cache", // Force fresh fetch for prefetching
...this.getFilteredHeaders(context?.headers),
},
timeout: 5000, // 5 second timeout for prefetch requests
redirect: "follow",
maxRedirects: 3,
};
// Use Node.js built-in fetch if available (Node 18+), otherwise use a fallback
let response;
let responseData;
if (typeof fetch !== "undefined") {
// Use native fetch
response = await fetch(fullUrl, requestOptions);
if (response.ok) {
// Try to get response data based on content type
const contentType = response.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
responseData = await response.json();
}
else if (contentType.includes("text/")) {
responseData = await response.text();
}
else {
// For binary data, just get the size
const buffer = await response.arrayBuffer();
responseData = {
size: buffer.byteLength,
type: "binary",
};
}
}
else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
}
else {
// Fallback using Node.js http/https modules
response = await this.makeHttpRequest(fullUrl, requestOptions);
responseData = response.data;
}
// Cache the prefetched content
await this.cachePrefetchedContent(url, responseData, response);
return {
success: true,
data: responseData,
statusCode: response.status || response.statusCode,
headers: response.headers,
};
}
catch (error) {
console.warn(`Prefetch failed for ${url}:`, error);
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
/**
* Filter headers to only include safe ones for prefetching
*/
getFilteredHeaders(headers) {
if (!headers)
return {};
const safeHeaders = {};
const allowedHeaders = [
"accept",
"accept-language",
"accept-encoding",
"user-agent",
];
for (const [key, value] of Object.entries(headers)) {
if (allowedHeaders.includes(key.toLowerCase()) &&
typeof value === "string") {
safeHeaders[key] = value;
}
}
return safeHeaders;
}
/**
* Make HTTP request using Node.js built-in modules
*/
async makeHttpRequest(url, options) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === "https:";
// Dynamically import http/https modules
const httpModule = isHttps ? require("https") : require("http");
const requestOptions = {
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: options.method || "GET",
headers: options.headers || {},
timeout: options.timeout || 5000,
};
const req = httpModule.request(requestOptions, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
resolve({
statusCode: res.statusCode,
headers: res.headers,
data: data,
});
});
});
req.on("error", (error) => {
reject(error);
});
req.on("timeout", () => {
req.destroy();
reject(new Error("Request timeout"));
});
req.end();
});
}
/**
* Cache the prefetched content
*/
async cachePrefetchedContent(url, data, response) {
try {
// Generate cache key for the prefetched content
const cacheKey = this.generatePrefetchCacheKey(url);
// Determine TTL based on response headers
const ttl = this.calculatePrefetchTTL(response);
// Store in cache with appropriate metadata
const cacheEntry = {
url,
data,
timestamp: Date.now(),
statusCode: response.status || response.statusCode,
headers: response.headers,
prefetched: true,
};
// Use the plugin's cache if available
if (this.cache) {
await this.cache.set(cacheKey, cacheEntry, { ttl });
}
}
catch (error) {
console.warn(`Failed to cache prefetched content for ${url}:`, error);
}
}
/**
* Generate cache key for prefetched content
*/
generatePrefetchCacheKey(url) {
return `prefetch:${url}`;
}
/**
* Calculate TTL for prefetched content based on response headers
*/
calculatePrefetchTTL(response) {
const headers = response.headers || {};
// Check Cache-Control header
const cacheControl = headers["cache-control"] || headers.get?.("cache-control");
if (cacheControl) {
const maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
if (maxAgeMatch) {
return parseInt(maxAgeMatch[1]) * 1000; // Convert to milliseconds
}
}
// Check Expires header
const expires = headers["expires"] || headers.get?.("expires");
if (expires) {
const expiresDate = new Date(expires);
const now = new Date();
if (expiresDate > now) {
return expiresDate.getTime() - now.getTime();
}
}
// Default TTL for prefetched content (5 minutes)
return 300000;
}
/**
* Update request pattern data
*/
updateRequestPattern(url, responseTime) {
const pattern = this.requestPatterns.get(url) || {
frequency: 0,
lastAccess: 0,
averageResponseTime: 0,
volatility: 0.5,
};
pattern.frequency++;
pattern.lastAccess = Date.now();
pattern.averageResponseTime =
(pattern.averageResponseTime * (pattern.frequency - 1) +
responseTime) /
pattern.frequency;
this.requestPatterns.set(url, pattern);
}
// ===== UTILITY METHODS =====
/**
* Normalize route for pattern tracking
*/
normalizeRoute(path) {
return path
.replace(/\/\d+/g, "/:id")
.replace(/\/[a-f0-9-]{36}/g, "/:uuid")
.replace(/\?.*$/, "");
}
/**
* Detect device type from user agent
*/
detectDeviceType(userAgent) {
if (/Mobile|Android|iPhone|iPad/.test(userAgent)) {
return "mobile";
}
if (/Tablet|iPad/.test(userAgent)) {
return "tablet";
}
return "desktop";
}
/**
* Check for complex query parameters
*/
hasComplexQueryParams(query) {
const complexParams = [
"search",
"filter",
"sort",
"timestamp",
"random",
];
return complexParams.some((param) => param in query);
}
/**
* Get top request patterns
*/
getTopRequestPatterns() {
const patterns = Array.from(this.requestPatterns.entries())
.sort((a, b) => b[1].frequency - a[1].frequency)
.slice(0, 10);
return patterns.map(([route, data]) => ({
route,
frequency: data.frequency,
averageResponseTime: data.averageResponseTime,
volatility: data.volatility,
}));
}
/**
* Suggest TTL optimizations
*/
suggestTTLOptimizations() {
const suggestions = [];
for (const [route, pattern] of this.requestPatterns.entries()) {
if (pattern.frequency > 50 && pattern.volatility < 0.1) {
suggestions.push(`Increase TTL for high-frequency, stable route: ${route}`);
}
if (pattern.volatility > 0.8) {
suggestions.push(`Decrease TTL for volatile route: ${route}`);
}
}
return suggestions;
}
/**
* Generate optimization suggestions
*/
generateOptimizationSuggestions() {
const suggestions = [];
const hitRate = this.cacheAnalytics.totalRequests > 0
? (this.cacheAnalytics.cacheHits /
this.cacheAnalytics.totalRequests) *
100
: 0;
if (hitRate < 30) {
suggestions.push("Consider increasing TTL values to improve hit rate");
}
if (hitRate > 90) {
suggestions.push("Excellent cache performance - consider expanding caching rules");
}
if (this.cacheAnalytics.averageMissTime >
this.cacheAnalytics.averageHitTime * 10) {
suggestions.push("High miss penalty - consider cache warming strategies");
}
return suggestions;
}
/**
* Identify related content for prefetching
*/
identifyRelatedContent(path) {
const related = [];
// Simple related content identification
if (path.startsWith("/api/users/")) {
related.push("/api/users/profile", "/api/users/preferences");
}
if (path.startsWith("/api/products/")) {
related.push("/api/products/categories", "/api/products/featured");
}
return related;
}
/**
* Setup analytics cleanup
*/
setupAnalyticsCleanup() {
// Reset analytics every hour
setInterval(() => {
this.resetAnalytics();
}, 3600000); // 1 hour
}
/**
* Setup dynamic TTL adjustment
*/
setupDynamicTTLAdjustment() {
// Analyze patterns every 10 minutes
setInterval(() => {
this.analyzeRequestPatterns();
}, 600000); // 10 minutes
}
/**
* Reset analytics
*/
resetAnalytics() {
// Keep some historical data, reset counters
this.cacheAnalytics.totalRequests = 0;
this.cacheAnalytics.cacheableRequests = 0;
this.cacheAnalytics.cacheHits = 0;
this.cacheAnalytics.cacheMisses = 0;
this.cacheAnalytics.cacheSkips = 0;
}
/**
* Analyze request patterns for optimization
*/
analyzeRequestPatterns() {
const now = Date.now();
// Clean up old patterns
for (const [route, pattern] of this.requestPatterns.entries()) {
if (now - pattern.lastAccess > 3600000) {
// 1 hour
this.requestPatterns.delete(route);
}
}
}
}
exports.SmartCachePlugin = SmartCachePlugin;
//# sourceMappingURL=SmartCachePlugin.js.map