xypriss 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -13
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/mods/security/src/index.js +35 -12
- package/dist/cjs/mods/security/src/index.js.map +1 -1
- package/dist/cjs/src/plugins/modules/PluginEngine.js +378 -0
- package/dist/cjs/src/plugins/modules/PluginEngine.js.map +1 -0
- package/dist/cjs/src/plugins/modules/PluginRegistry.js +339 -0
- package/dist/cjs/src/plugins/modules/PluginRegistry.js.map +1 -0
- package/dist/cjs/src/plugins/modules/builtin/JWTAuthPlugin.js +591 -0
- package/dist/cjs/src/plugins/modules/builtin/JWTAuthPlugin.js.map +1 -0
- package/dist/cjs/src/plugins/modules/builtin/ResponseTimePlugin.js +413 -0
- package/dist/cjs/src/plugins/modules/builtin/ResponseTimePlugin.js.map +1 -0
- package/dist/cjs/src/plugins/modules/builtin/SmartCachePlugin.js +843 -0
- package/dist/cjs/src/plugins/modules/builtin/SmartCachePlugin.js.map +1 -0
- package/dist/cjs/src/plugins/modules/core/CachePlugin.js +1975 -0
- package/dist/cjs/src/plugins/modules/core/CachePlugin.js.map +1 -0
- package/dist/cjs/src/plugins/modules/core/PerformancePlugin.js +894 -0
- package/dist/cjs/src/plugins/modules/core/PerformancePlugin.js.map +1 -0
- package/dist/cjs/src/plugins/modules/core/SecurityPlugin.js +799 -0
- package/dist/cjs/src/plugins/modules/core/SecurityPlugin.js.map +1 -0
- package/dist/cjs/src/plugins/modules/types/PluginTypes.js +47 -0
- package/dist/cjs/src/plugins/modules/types/PluginTypes.js.map +1 -0
- package/dist/cjs/src/server/FastServer.js +22 -3
- package/dist/cjs/src/server/FastServer.js.map +1 -1
- package/dist/cjs/src/server/components/fastapi/PluginManager.js +5 -5
- package/dist/cjs/src/server/components/fastapi/PluginManager.js.map +1 -1
- package/dist/cjs/src/server/components/fastapi/RequestProcessor.js +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/mods/security/src/index.js +14 -10
- package/dist/esm/mods/security/src/index.js.map +1 -1
- package/dist/esm/src/plugins/modules/PluginEngine.js +376 -0
- package/dist/esm/src/plugins/modules/PluginEngine.js.map +1 -0
- package/dist/esm/src/plugins/modules/PluginRegistry.js +337 -0
- package/dist/esm/src/plugins/modules/PluginRegistry.js.map +1 -0
- package/dist/esm/src/plugins/modules/builtin/JWTAuthPlugin.js +589 -0
- package/dist/esm/src/plugins/modules/builtin/JWTAuthPlugin.js.map +1 -0
- package/dist/esm/src/plugins/modules/builtin/ResponseTimePlugin.js +411 -0
- package/dist/esm/src/plugins/modules/builtin/ResponseTimePlugin.js.map +1 -0
- package/dist/esm/src/plugins/modules/builtin/SmartCachePlugin.js +841 -0
- package/dist/esm/src/plugins/modules/builtin/SmartCachePlugin.js.map +1 -0
- package/dist/esm/src/plugins/modules/core/CachePlugin.js +1973 -0
- package/dist/esm/src/plugins/modules/core/CachePlugin.js.map +1 -0
- package/dist/esm/src/plugins/modules/core/PerformancePlugin.js +872 -0
- package/dist/esm/src/plugins/modules/core/PerformancePlugin.js.map +1 -0
- package/dist/esm/src/plugins/modules/core/SecurityPlugin.js +797 -0
- package/dist/esm/src/plugins/modules/core/SecurityPlugin.js.map +1 -0
- package/dist/esm/src/plugins/modules/types/PluginTypes.js +47 -0
- package/dist/esm/src/plugins/modules/types/PluginTypes.js.map +1 -0
- package/dist/esm/src/server/FastServer.js +22 -3
- package/dist/esm/src/server/FastServer.js.map +1 -1
- package/dist/esm/src/server/components/fastapi/PluginManager.js +5 -5
- package/dist/esm/src/server/components/fastapi/PluginManager.js.map +1 -1
- package/dist/esm/src/server/components/fastapi/RequestProcessor.js +1 -1
- package/dist/index.d.ts +5 -0
- package/package.json +4 -4
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
import { CachePlugin } from '../core/CachePlugin.js';
|
|
2
|
+
import { PluginPriority } from '../types/PluginTypes.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Smart Cache Plugin
|
|
6
|
+
*
|
|
7
|
+
* Intelligent caching plugin with <0.5ms execution overhead
|
|
8
|
+
* leveraging XyPrissJS cache systems for optimal performance.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Smart Cache Plugin for intelligent request caching
|
|
12
|
+
*/
|
|
13
|
+
class SmartCachePlugin extends CachePlugin {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this.id = "nehonix.ftfy.cache";
|
|
17
|
+
this.name = "Smart Cache Plugin";
|
|
18
|
+
this.version = "1.0.0";
|
|
19
|
+
this.priority = PluginPriority.HIGH;
|
|
20
|
+
// Cache configuration
|
|
21
|
+
this.cacheStrategy = "hybrid";
|
|
22
|
+
this.compressionEnabled = true;
|
|
23
|
+
this.encryptionEnabled = true; // Keep encryption for security
|
|
24
|
+
// Smart caching rules
|
|
25
|
+
this.cachingRules = new Map();
|
|
26
|
+
// Cache analytics
|
|
27
|
+
this.cacheAnalytics = {
|
|
28
|
+
totalRequests: 0,
|
|
29
|
+
cacheableRequests: 0,
|
|
30
|
+
cacheHits: 0,
|
|
31
|
+
cacheMisses: 0,
|
|
32
|
+
cacheSkips: 0,
|
|
33
|
+
averageHitTime: 0,
|
|
34
|
+
averageMissTime: 0,
|
|
35
|
+
compressionSavings: 0,
|
|
36
|
+
};
|
|
37
|
+
// Dynamic TTL adjustment based on request patterns
|
|
38
|
+
this.requestPatterns = new Map();
|
|
39
|
+
// Background prefetching queue and worker
|
|
40
|
+
this.prefetchQueue = [];
|
|
41
|
+
this.prefetchWorkerActive = false;
|
|
42
|
+
this.prefetchStats = {
|
|
43
|
+
totalPrefetched: 0,
|
|
44
|
+
successfulPrefetches: 0,
|
|
45
|
+
failedPrefetches: 0,
|
|
46
|
+
averagePrefetchTime: 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Initialize smart cache plugin
|
|
51
|
+
*/
|
|
52
|
+
async initializeCachePlugin(context) {
|
|
53
|
+
// Setup default caching rules
|
|
54
|
+
this.setupDefaultCachingRules();
|
|
55
|
+
// Configure custom rules from settings
|
|
56
|
+
if (context.config.customSettings.cachingRules) {
|
|
57
|
+
this.configureCachingRules(context.config.customSettings.cachingRules);
|
|
58
|
+
}
|
|
59
|
+
// Setup cache analytics cleanup
|
|
60
|
+
this.setupAnalyticsCleanup();
|
|
61
|
+
// Setup dynamic TTL adjustment
|
|
62
|
+
this.setupDynamicTTLAdjustment();
|
|
63
|
+
context.logger.info("Smart Cache Plugin initialized with intelligent caching rules");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check if request should be cached (plugin-specific logic)
|
|
67
|
+
*/
|
|
68
|
+
shouldCacheRequest(context) {
|
|
69
|
+
const { req } = context;
|
|
70
|
+
// Apply smart caching rules
|
|
71
|
+
for (const [ruleName, rule] of this.cachingRules.entries()) {
|
|
72
|
+
if (rule.enabled && rule.pattern.test(req.path)) {
|
|
73
|
+
// Check additional conditions
|
|
74
|
+
if (this.shouldApplyRule(context, rule)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Fallback to intelligent heuristics
|
|
80
|
+
return this.applyIntelligentCaching(context);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get custom cache key components
|
|
84
|
+
*/
|
|
85
|
+
getCustomKeyComponents(context) {
|
|
86
|
+
const { req } = context;
|
|
87
|
+
const components = [];
|
|
88
|
+
// Add user-specific components for personalized content
|
|
89
|
+
if (context.security.isAuthenticated) {
|
|
90
|
+
components.push(`user:${context.security.userId}`);
|
|
91
|
+
// Add role-based caching
|
|
92
|
+
if (context.security.roles.length > 0) {
|
|
93
|
+
components.push(`roles:${context.security.roles.sort().join(",")}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Add device type for responsive caching
|
|
97
|
+
const userAgent = req.headers["user-agent"];
|
|
98
|
+
if (userAgent) {
|
|
99
|
+
const deviceType = this.detectDeviceType(userAgent);
|
|
100
|
+
components.push(`device:${deviceType}`);
|
|
101
|
+
}
|
|
102
|
+
// Add language for i18n caching
|
|
103
|
+
const acceptLanguage = req.headers["accept-language"];
|
|
104
|
+
if (acceptLanguage) {
|
|
105
|
+
const primaryLanguage = acceptLanguage.split(",")[0].split("-")[0];
|
|
106
|
+
components.push(`lang:${primaryLanguage}`);
|
|
107
|
+
}
|
|
108
|
+
// Add API version for versioned APIs
|
|
109
|
+
const apiVersion = req.headers["api-version"] || req.query.version;
|
|
110
|
+
if (apiVersion) {
|
|
111
|
+
components.push(`version:${apiVersion}`);
|
|
112
|
+
}
|
|
113
|
+
return components;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get custom TTL for request
|
|
117
|
+
*/
|
|
118
|
+
getCustomTTL(context) {
|
|
119
|
+
const { req } = context;
|
|
120
|
+
const route = this.normalizeRoute(req.path);
|
|
121
|
+
// Check if we have pattern data for dynamic TTL
|
|
122
|
+
const pattern = this.requestPatterns.get(route);
|
|
123
|
+
if (pattern) {
|
|
124
|
+
return this.calculateDynamicTTL(pattern);
|
|
125
|
+
}
|
|
126
|
+
// Apply rule-based TTL
|
|
127
|
+
for (const [ruleName, rule] of this.cachingRules.entries()) {
|
|
128
|
+
if (rule.enabled && rule.pattern.test(req.path)) {
|
|
129
|
+
return rule.ttl;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Default TTL based on content type
|
|
133
|
+
return this.getDefaultTTLByContentType(req.path);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Handle custom cache operations
|
|
137
|
+
*/
|
|
138
|
+
async handleCustomCacheOperation(context, operation) {
|
|
139
|
+
switch (operation) {
|
|
140
|
+
case "analyze":
|
|
141
|
+
return await this.analyzeCachePerformance(context);
|
|
142
|
+
case "optimize":
|
|
143
|
+
return await this.optimizeCacheStrategy(context);
|
|
144
|
+
case "prefetch":
|
|
145
|
+
return await this.prefetchRelatedContent(context);
|
|
146
|
+
default:
|
|
147
|
+
return { operation, supported: false };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Precompile cache operations
|
|
152
|
+
*/
|
|
153
|
+
async precompileCacheOperations() {
|
|
154
|
+
// Pre-warm route normalization
|
|
155
|
+
this.normalizeRoute("/api/users/123");
|
|
156
|
+
// Pre-warm device detection
|
|
157
|
+
this.detectDeviceType("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
|
158
|
+
// Pre-warm TTL calculation
|
|
159
|
+
this.calculateDynamicTTL({
|
|
160
|
+
frequency: 10,
|
|
161
|
+
lastAccess: Date.now(),
|
|
162
|
+
averageResponseTime: 100,
|
|
163
|
+
volatility: 0.1,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// ===== SMART CACHING LOGIC =====
|
|
167
|
+
/**
|
|
168
|
+
* Setup default caching rules
|
|
169
|
+
*/
|
|
170
|
+
setupDefaultCachingRules() {
|
|
171
|
+
// Static assets - long TTL
|
|
172
|
+
this.cachingRules.set("static", {
|
|
173
|
+
pattern: /\.(css|js|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$/,
|
|
174
|
+
ttl: 86400000, // 24 hours
|
|
175
|
+
enabled: true,
|
|
176
|
+
compression: true,
|
|
177
|
+
tags: ["static"],
|
|
178
|
+
});
|
|
179
|
+
// API responses - medium TTL
|
|
180
|
+
this.cachingRules.set("api", {
|
|
181
|
+
pattern: /^\/api\/(?!auth|admin)/,
|
|
182
|
+
ttl: 300000, // 5 minutes
|
|
183
|
+
enabled: true,
|
|
184
|
+
compression: true,
|
|
185
|
+
tags: ["api"],
|
|
186
|
+
});
|
|
187
|
+
// Public pages - short TTL
|
|
188
|
+
this.cachingRules.set("public", {
|
|
189
|
+
pattern: /^\/(?!admin|dashboard|profile)/,
|
|
190
|
+
ttl: 60000, // 1 minute
|
|
191
|
+
enabled: true,
|
|
192
|
+
compression: true,
|
|
193
|
+
tags: ["public"],
|
|
194
|
+
});
|
|
195
|
+
// User-specific content - very short TTL
|
|
196
|
+
this.cachingRules.set("user", {
|
|
197
|
+
pattern: /^\/(profile|dashboard|settings)/,
|
|
198
|
+
ttl: 30000, // 30 seconds
|
|
199
|
+
enabled: true,
|
|
200
|
+
compression: false,
|
|
201
|
+
tags: ["user"],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Configure custom caching rules
|
|
206
|
+
*/
|
|
207
|
+
configureCachingRules(rules) {
|
|
208
|
+
for (const rule of rules) {
|
|
209
|
+
this.cachingRules.set(rule.name, {
|
|
210
|
+
pattern: new RegExp(rule.pattern),
|
|
211
|
+
ttl: rule.ttl || 300000,
|
|
212
|
+
enabled: rule.enabled !== false,
|
|
213
|
+
compression: rule.compression !== false,
|
|
214
|
+
tags: rule.tags || [],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Check if caching rule should be applied
|
|
220
|
+
*/
|
|
221
|
+
shouldApplyRule(context, rule) {
|
|
222
|
+
const { req } = context;
|
|
223
|
+
// Don't cache authenticated requests for public rules
|
|
224
|
+
if (rule.tags.includes("public") && context.security.isAuthenticated) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
// Don't cache if request has cache-control: no-cache
|
|
228
|
+
const cacheControl = req.headers["cache-control"];
|
|
229
|
+
if (cacheControl && cacheControl.includes("no-cache")) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
// Don't cache if request has pragma: no-cache
|
|
233
|
+
const pragma = req.headers.pragma;
|
|
234
|
+
if (pragma && pragma.includes("no-cache")) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Apply intelligent caching heuristics
|
|
241
|
+
*/
|
|
242
|
+
applyIntelligentCaching(context) {
|
|
243
|
+
const { req } = context;
|
|
244
|
+
// Analyze request characteristics
|
|
245
|
+
const hasQueryParams = Object.keys(req.query).length > 0;
|
|
246
|
+
const hasBody = req.body && Object.keys(req.body).length > 0;
|
|
247
|
+
const isIdempotent = ["GET", "HEAD", "OPTIONS"].includes(req.method);
|
|
248
|
+
// Don't cache non-idempotent requests
|
|
249
|
+
if (!isIdempotent) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
// Don't cache requests with complex query parameters
|
|
253
|
+
if (hasQueryParams && this.hasComplexQueryParams(req.query)) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
// Cache simple GET requests
|
|
257
|
+
if (req.method === "GET" && !hasBody) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Calculate dynamic TTL based on request patterns
|
|
264
|
+
*/
|
|
265
|
+
calculateDynamicTTL(pattern) {
|
|
266
|
+
const baseTime = 300000; // 5 minutes base
|
|
267
|
+
// Adjust based on frequency (more frequent = longer cache)
|
|
268
|
+
const frequencyMultiplier = Math.min(pattern.frequency / 10, 2);
|
|
269
|
+
// Adjust based on volatility (more volatile = shorter cache)
|
|
270
|
+
const volatilityMultiplier = Math.max(1 - pattern.volatility, 0.1);
|
|
271
|
+
// Adjust based on response time (slower = longer cache)
|
|
272
|
+
const responseTimeMultiplier = Math.min(pattern.averageResponseTime / 100, 3);
|
|
273
|
+
return Math.round(baseTime *
|
|
274
|
+
frequencyMultiplier *
|
|
275
|
+
volatilityMultiplier *
|
|
276
|
+
responseTimeMultiplier);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Get default TTL by content type
|
|
280
|
+
*/
|
|
281
|
+
getDefaultTTLByContentType(path) {
|
|
282
|
+
if (path.match(/\.(css|js)$/)) {
|
|
283
|
+
return 3600000; // 1 hour for CSS/JS
|
|
284
|
+
}
|
|
285
|
+
if (path.match(/\.(png|jpg|jpeg|gif|svg|ico)$/)) {
|
|
286
|
+
return 86400000; // 24 hours for images
|
|
287
|
+
}
|
|
288
|
+
if (path.startsWith("/api/")) {
|
|
289
|
+
return 300000; // 5 minutes for API
|
|
290
|
+
}
|
|
291
|
+
return 60000; // 1 minute default
|
|
292
|
+
}
|
|
293
|
+
// ===== ANALYTICS AND OPTIMIZATION =====
|
|
294
|
+
/**
|
|
295
|
+
* Analyze cache performance
|
|
296
|
+
*/
|
|
297
|
+
async analyzeCachePerformance(context) {
|
|
298
|
+
const hitRate = this.cacheAnalytics.totalRequests > 0
|
|
299
|
+
? (this.cacheAnalytics.cacheHits /
|
|
300
|
+
this.cacheAnalytics.totalRequests) *
|
|
301
|
+
100
|
|
302
|
+
: 0;
|
|
303
|
+
const cacheableRate = this.cacheAnalytics.totalRequests > 0
|
|
304
|
+
? (this.cacheAnalytics.cacheableRequests /
|
|
305
|
+
this.cacheAnalytics.totalRequests) *
|
|
306
|
+
100
|
|
307
|
+
: 0;
|
|
308
|
+
return {
|
|
309
|
+
hitRate: Math.round(hitRate * 100) / 100,
|
|
310
|
+
cacheableRate: Math.round(cacheableRate * 100) / 100,
|
|
311
|
+
totalRequests: this.cacheAnalytics.totalRequests,
|
|
312
|
+
cacheHits: this.cacheAnalytics.cacheHits,
|
|
313
|
+
cacheMisses: this.cacheAnalytics.cacheMisses,
|
|
314
|
+
averageHitTime: this.cacheAnalytics.averageHitTime,
|
|
315
|
+
averageMissTime: this.cacheAnalytics.averageMissTime,
|
|
316
|
+
compressionSavings: this.cacheAnalytics.compressionSavings,
|
|
317
|
+
topPatterns: this.getTopRequestPatterns(),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Optimize cache strategy
|
|
322
|
+
*/
|
|
323
|
+
async optimizeCacheStrategy(context) {
|
|
324
|
+
const optimizations = [];
|
|
325
|
+
// Analyze hit rates by rule
|
|
326
|
+
for (const [ruleName, rule] of this.cachingRules.entries()) {
|
|
327
|
+
// Suggest optimizations based on performance
|
|
328
|
+
if (rule.enabled) {
|
|
329
|
+
optimizations.push(`Rule '${ruleName}' is active`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Suggest TTL adjustments
|
|
333
|
+
const ttlSuggestions = this.suggestTTLOptimizations();
|
|
334
|
+
optimizations.push(...ttlSuggestions);
|
|
335
|
+
return {
|
|
336
|
+
optimizations,
|
|
337
|
+
suggestions: this.generateSmartOptimizationSuggestions(),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Prefetch related content
|
|
342
|
+
*/
|
|
343
|
+
async prefetchRelatedContent(context) {
|
|
344
|
+
const { req } = context;
|
|
345
|
+
const relatedUrls = this.identifyRelatedContent(req.path);
|
|
346
|
+
// Queue related URLs for background prefetching
|
|
347
|
+
const queuedUrls = [];
|
|
348
|
+
for (const url of relatedUrls) {
|
|
349
|
+
const priority = this.calculatePrefetchPriority(url, context);
|
|
350
|
+
// Only queue high-priority URLs to avoid overwhelming the system
|
|
351
|
+
if (priority > 0.5) {
|
|
352
|
+
this.queueForPrefetch(url, priority, context);
|
|
353
|
+
queuedUrls.push(url);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Start background prefetch worker if not already active
|
|
357
|
+
if (!this.prefetchWorkerActive && this.prefetchQueue.length > 0) {
|
|
358
|
+
this.startPrefetchWorker();
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
prefetched: queuedUrls.length,
|
|
362
|
+
urls: queuedUrls,
|
|
363
|
+
queueSize: this.prefetchQueue.length,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Queue a URL for background prefetching
|
|
368
|
+
*/
|
|
369
|
+
queueForPrefetch(url, priority, context) {
|
|
370
|
+
// Avoid duplicate entries
|
|
371
|
+
const existingIndex = this.prefetchQueue.findIndex((item) => item.url === url);
|
|
372
|
+
if (existingIndex >= 0) {
|
|
373
|
+
// Update priority if higher
|
|
374
|
+
if (this.prefetchQueue[existingIndex].priority < priority) {
|
|
375
|
+
this.prefetchQueue[existingIndex].priority = priority;
|
|
376
|
+
this.prefetchQueue[existingIndex].timestamp = Date.now();
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// Add to queue
|
|
381
|
+
this.prefetchQueue.push({
|
|
382
|
+
url,
|
|
383
|
+
priority,
|
|
384
|
+
timestamp: Date.now(),
|
|
385
|
+
context: {
|
|
386
|
+
method: context.req.method,
|
|
387
|
+
headers: context.req.headers,
|
|
388
|
+
baseUrl: `${context.req.protocol}://${context.req.get("host")}`,
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
// Sort by priority (highest first)
|
|
392
|
+
this.prefetchQueue.sort((a, b) => b.priority - a.priority);
|
|
393
|
+
// Limit queue size to prevent memory issues
|
|
394
|
+
if (this.prefetchQueue.length > 100) {
|
|
395
|
+
this.prefetchQueue = this.prefetchQueue.slice(0, 100);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Calculate prefetch priority for a URL
|
|
400
|
+
*/
|
|
401
|
+
calculatePrefetchPriority(url, context) {
|
|
402
|
+
let priority = 0.3; // Base priority
|
|
403
|
+
// Check request patterns
|
|
404
|
+
const pattern = this.requestPatterns.get(url);
|
|
405
|
+
if (pattern) {
|
|
406
|
+
// Higher frequency = higher priority
|
|
407
|
+
priority += Math.min(pattern.frequency / 100, 0.4);
|
|
408
|
+
// Recent access = higher priority
|
|
409
|
+
const timeSinceAccess = Date.now() - pattern.lastAccess;
|
|
410
|
+
if (timeSinceAccess < 300000) {
|
|
411
|
+
// 5 minutes
|
|
412
|
+
priority += 0.2;
|
|
413
|
+
}
|
|
414
|
+
// Lower volatility = higher priority (more stable content)
|
|
415
|
+
priority += (1 - pattern.volatility) * 0.1;
|
|
416
|
+
}
|
|
417
|
+
// Check if URL matches high-priority patterns
|
|
418
|
+
if (url.includes("/api/") || url.includes("/static/")) {
|
|
419
|
+
priority += 0.2;
|
|
420
|
+
}
|
|
421
|
+
// Check if it's a common resource type
|
|
422
|
+
if (url.match(/\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2)$/)) {
|
|
423
|
+
priority += 0.3;
|
|
424
|
+
}
|
|
425
|
+
return Math.min(priority, 1.0);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Start the background prefetch worker
|
|
429
|
+
*/
|
|
430
|
+
async startPrefetchWorker() {
|
|
431
|
+
if (this.prefetchWorkerActive)
|
|
432
|
+
return;
|
|
433
|
+
this.prefetchWorkerActive = true;
|
|
434
|
+
try {
|
|
435
|
+
while (this.prefetchQueue.length > 0) {
|
|
436
|
+
const item = this.prefetchQueue.shift();
|
|
437
|
+
if (!item)
|
|
438
|
+
break;
|
|
439
|
+
// Skip items that are too old (older than 5 minutes)
|
|
440
|
+
if (Date.now() - item.timestamp > 300000) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
await this.performPrefetch(item);
|
|
444
|
+
// Small delay to prevent overwhelming the server
|
|
445
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
console.error("Prefetch worker error:", error);
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
this.prefetchWorkerActive = false;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Perform actual prefetch operation
|
|
457
|
+
*/
|
|
458
|
+
async performPrefetch(item) {
|
|
459
|
+
const startTime = Date.now();
|
|
460
|
+
try {
|
|
461
|
+
// Perform actual HTTP request for prefetching to warm up the cache
|
|
462
|
+
const response = await this.performActualPrefetchRequest(item.url, item.context);
|
|
463
|
+
if (response.success) {
|
|
464
|
+
this.prefetchStats.successfulPrefetches++;
|
|
465
|
+
// Update request patterns
|
|
466
|
+
this.updateRequestPattern(item.url, Date.now() - startTime);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
this.prefetchStats.failedPrefetches++;
|
|
470
|
+
}
|
|
471
|
+
this.prefetchStats.totalPrefetched++;
|
|
472
|
+
// Update average prefetch time
|
|
473
|
+
const prefetchTime = Date.now() - startTime;
|
|
474
|
+
this.prefetchStats.averagePrefetchTime =
|
|
475
|
+
(this.prefetchStats.averagePrefetchTime *
|
|
476
|
+
(this.prefetchStats.totalPrefetched - 1) +
|
|
477
|
+
prefetchTime) /
|
|
478
|
+
this.prefetchStats.totalPrefetched;
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
this.prefetchStats.failedPrefetches++;
|
|
482
|
+
this.prefetchStats.totalPrefetched++;
|
|
483
|
+
console.error(`Prefetch failed for ${item.url}:`, error);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Perform actual prefetch request with real HTTP call
|
|
488
|
+
*/
|
|
489
|
+
async performActualPrefetchRequest(url, context) {
|
|
490
|
+
try {
|
|
491
|
+
// Construct full URL if relative
|
|
492
|
+
let fullUrl = url;
|
|
493
|
+
if (context?.baseUrl && !url.startsWith("http")) {
|
|
494
|
+
fullUrl = `${context.baseUrl}${url.startsWith("/") ? url : "/" + url}`;
|
|
495
|
+
}
|
|
496
|
+
// Prepare request options
|
|
497
|
+
const requestOptions = {
|
|
498
|
+
method: context?.method || "GET",
|
|
499
|
+
headers: {
|
|
500
|
+
"User-Agent": "XyPrissJS-SmartCache/1.0",
|
|
501
|
+
Accept: "*/*",
|
|
502
|
+
"Cache-Control": "no-cache", // Force fresh fetch for prefetching
|
|
503
|
+
...this.getFilteredHeaders(context?.headers),
|
|
504
|
+
},
|
|
505
|
+
timeout: 5000, // 5 second timeout for prefetch requests
|
|
506
|
+
redirect: "follow",
|
|
507
|
+
maxRedirects: 3,
|
|
508
|
+
};
|
|
509
|
+
// Use Node.js built-in fetch if available (Node 18+), otherwise use a fallback
|
|
510
|
+
let response;
|
|
511
|
+
let responseData;
|
|
512
|
+
if (typeof fetch !== "undefined") {
|
|
513
|
+
// Use native fetch
|
|
514
|
+
response = await fetch(fullUrl, requestOptions);
|
|
515
|
+
if (response.ok) {
|
|
516
|
+
// Try to get response data based on content type
|
|
517
|
+
const contentType = response.headers.get("content-type") || "";
|
|
518
|
+
if (contentType.includes("application/json")) {
|
|
519
|
+
responseData = await response.json();
|
|
520
|
+
}
|
|
521
|
+
else if (contentType.includes("text/")) {
|
|
522
|
+
responseData = await response.text();
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
// For binary data, just get the size
|
|
526
|
+
const buffer = await response.arrayBuffer();
|
|
527
|
+
responseData = {
|
|
528
|
+
size: buffer.byteLength,
|
|
529
|
+
type: "binary",
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
// Fallback using Node.js http/https modules
|
|
539
|
+
response = await this.makeHttpRequest(fullUrl, requestOptions);
|
|
540
|
+
responseData = response.data;
|
|
541
|
+
}
|
|
542
|
+
// Cache the prefetched content
|
|
543
|
+
await this.cachePrefetchedContent(url, responseData, response);
|
|
544
|
+
return {
|
|
545
|
+
success: true,
|
|
546
|
+
data: responseData,
|
|
547
|
+
statusCode: response.status || response.statusCode,
|
|
548
|
+
headers: response.headers,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
console.warn(`Prefetch failed for ${url}:`, error);
|
|
553
|
+
return {
|
|
554
|
+
success: false,
|
|
555
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Filter headers to only include safe ones for prefetching
|
|
561
|
+
*/
|
|
562
|
+
getFilteredHeaders(headers) {
|
|
563
|
+
if (!headers)
|
|
564
|
+
return {};
|
|
565
|
+
const safeHeaders = {};
|
|
566
|
+
const allowedHeaders = [
|
|
567
|
+
"accept",
|
|
568
|
+
"accept-language",
|
|
569
|
+
"accept-encoding",
|
|
570
|
+
"user-agent",
|
|
571
|
+
];
|
|
572
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
573
|
+
if (allowedHeaders.includes(key.toLowerCase()) &&
|
|
574
|
+
typeof value === "string") {
|
|
575
|
+
safeHeaders[key] = value;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return safeHeaders;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Make HTTP request using Node.js built-in modules
|
|
582
|
+
*/
|
|
583
|
+
async makeHttpRequest(url, options) {
|
|
584
|
+
return new Promise((resolve, reject) => {
|
|
585
|
+
const urlObj = new URL(url);
|
|
586
|
+
const isHttps = urlObj.protocol === "https:";
|
|
587
|
+
// Dynamically import http/https modules
|
|
588
|
+
const httpModule = isHttps ? require("https") : require("http");
|
|
589
|
+
const requestOptions = {
|
|
590
|
+
hostname: urlObj.hostname,
|
|
591
|
+
port: urlObj.port || (isHttps ? 443 : 80),
|
|
592
|
+
path: urlObj.pathname + urlObj.search,
|
|
593
|
+
method: options.method || "GET",
|
|
594
|
+
headers: options.headers || {},
|
|
595
|
+
timeout: options.timeout || 5000,
|
|
596
|
+
};
|
|
597
|
+
const req = httpModule.request(requestOptions, (res) => {
|
|
598
|
+
let data = "";
|
|
599
|
+
res.on("data", (chunk) => {
|
|
600
|
+
data += chunk;
|
|
601
|
+
});
|
|
602
|
+
res.on("end", () => {
|
|
603
|
+
resolve({
|
|
604
|
+
statusCode: res.statusCode,
|
|
605
|
+
headers: res.headers,
|
|
606
|
+
data: data,
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
req.on("error", (error) => {
|
|
611
|
+
reject(error);
|
|
612
|
+
});
|
|
613
|
+
req.on("timeout", () => {
|
|
614
|
+
req.destroy();
|
|
615
|
+
reject(new Error("Request timeout"));
|
|
616
|
+
});
|
|
617
|
+
req.end();
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Cache the prefetched content
|
|
622
|
+
*/
|
|
623
|
+
async cachePrefetchedContent(url, data, response) {
|
|
624
|
+
try {
|
|
625
|
+
// Generate cache key for the prefetched content
|
|
626
|
+
const cacheKey = this.generatePrefetchCacheKey(url);
|
|
627
|
+
// Determine TTL based on response headers
|
|
628
|
+
const ttl = this.calculatePrefetchTTL(response);
|
|
629
|
+
// Store in cache with appropriate metadata
|
|
630
|
+
const cacheEntry = {
|
|
631
|
+
url,
|
|
632
|
+
data,
|
|
633
|
+
timestamp: Date.now(),
|
|
634
|
+
statusCode: response.status || response.statusCode,
|
|
635
|
+
headers: response.headers,
|
|
636
|
+
prefetched: true,
|
|
637
|
+
};
|
|
638
|
+
// Use the plugin's cache if available
|
|
639
|
+
if (this.cache) {
|
|
640
|
+
await this.cache.set(cacheKey, cacheEntry, { ttl });
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
console.warn(`Failed to cache prefetched content for ${url}:`, error);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Generate cache key for prefetched content
|
|
649
|
+
*/
|
|
650
|
+
generatePrefetchCacheKey(url) {
|
|
651
|
+
return `prefetch:${url}`;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Calculate TTL for prefetched content based on response headers
|
|
655
|
+
*/
|
|
656
|
+
calculatePrefetchTTL(response) {
|
|
657
|
+
const headers = response.headers || {};
|
|
658
|
+
// Check Cache-Control header
|
|
659
|
+
const cacheControl = headers["cache-control"] || headers.get?.("cache-control");
|
|
660
|
+
if (cacheControl) {
|
|
661
|
+
const maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
|
|
662
|
+
if (maxAgeMatch) {
|
|
663
|
+
return parseInt(maxAgeMatch[1]) * 1000; // Convert to milliseconds
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// Check Expires header
|
|
667
|
+
const expires = headers["expires"] || headers.get?.("expires");
|
|
668
|
+
if (expires) {
|
|
669
|
+
const expiresDate = new Date(expires);
|
|
670
|
+
const now = new Date();
|
|
671
|
+
if (expiresDate > now) {
|
|
672
|
+
return expiresDate.getTime() - now.getTime();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// Default TTL for prefetched content (5 minutes)
|
|
676
|
+
return 300000;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Update request pattern data
|
|
680
|
+
*/
|
|
681
|
+
updateRequestPattern(url, responseTime) {
|
|
682
|
+
const pattern = this.requestPatterns.get(url) || {
|
|
683
|
+
frequency: 0,
|
|
684
|
+
lastAccess: 0,
|
|
685
|
+
averageResponseTime: 0,
|
|
686
|
+
volatility: 0.5,
|
|
687
|
+
};
|
|
688
|
+
pattern.frequency++;
|
|
689
|
+
pattern.lastAccess = Date.now();
|
|
690
|
+
pattern.averageResponseTime =
|
|
691
|
+
(pattern.averageResponseTime * (pattern.frequency - 1) +
|
|
692
|
+
responseTime) /
|
|
693
|
+
pattern.frequency;
|
|
694
|
+
this.requestPatterns.set(url, pattern);
|
|
695
|
+
}
|
|
696
|
+
// ===== UTILITY METHODS =====
|
|
697
|
+
/**
|
|
698
|
+
* Normalize route for pattern tracking
|
|
699
|
+
*/
|
|
700
|
+
normalizeRoute(path) {
|
|
701
|
+
return path
|
|
702
|
+
.replace(/\/\d+/g, "/:id")
|
|
703
|
+
.replace(/\/[a-f0-9-]{36}/g, "/:uuid")
|
|
704
|
+
.replace(/\?.*$/, "");
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Detect device type from user agent
|
|
708
|
+
*/
|
|
709
|
+
detectDeviceType(userAgent) {
|
|
710
|
+
if (/Mobile|Android|iPhone|iPad/.test(userAgent)) {
|
|
711
|
+
return "mobile";
|
|
712
|
+
}
|
|
713
|
+
if (/Tablet|iPad/.test(userAgent)) {
|
|
714
|
+
return "tablet";
|
|
715
|
+
}
|
|
716
|
+
return "desktop";
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Check for complex query parameters
|
|
720
|
+
*/
|
|
721
|
+
hasComplexQueryParams(query) {
|
|
722
|
+
const complexParams = [
|
|
723
|
+
"search",
|
|
724
|
+
"filter",
|
|
725
|
+
"sort",
|
|
726
|
+
"timestamp",
|
|
727
|
+
"random",
|
|
728
|
+
];
|
|
729
|
+
return complexParams.some((param) => param in query);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Get top request patterns
|
|
733
|
+
*/
|
|
734
|
+
getTopRequestPatterns() {
|
|
735
|
+
const patterns = Array.from(this.requestPatterns.entries())
|
|
736
|
+
.sort((a, b) => b[1].frequency - a[1].frequency)
|
|
737
|
+
.slice(0, 10);
|
|
738
|
+
return patterns.map(([route, data]) => ({
|
|
739
|
+
route,
|
|
740
|
+
frequency: data.frequency,
|
|
741
|
+
averageResponseTime: data.averageResponseTime,
|
|
742
|
+
volatility: data.volatility,
|
|
743
|
+
}));
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Suggest TTL optimizations
|
|
747
|
+
*/
|
|
748
|
+
suggestTTLOptimizations() {
|
|
749
|
+
const suggestions = [];
|
|
750
|
+
for (const [route, pattern] of this.requestPatterns.entries()) {
|
|
751
|
+
if (pattern.frequency > 50 && pattern.volatility < 0.1) {
|
|
752
|
+
suggestions.push(`Increase TTL for high-frequency, stable route: ${route}`);
|
|
753
|
+
}
|
|
754
|
+
if (pattern.volatility > 0.8) {
|
|
755
|
+
suggestions.push(`Decrease TTL for volatile route: ${route}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return suggestions;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Generate optimization suggestions
|
|
762
|
+
*/
|
|
763
|
+
generateSmartOptimizationSuggestions() {
|
|
764
|
+
const suggestions = [];
|
|
765
|
+
const hitRate = this.cacheAnalytics.totalRequests > 0
|
|
766
|
+
? (this.cacheAnalytics.cacheHits /
|
|
767
|
+
this.cacheAnalytics.totalRequests) *
|
|
768
|
+
100
|
|
769
|
+
: 0;
|
|
770
|
+
if (hitRate < 30) {
|
|
771
|
+
suggestions.push("Consider increasing TTL values to improve hit rate");
|
|
772
|
+
}
|
|
773
|
+
if (hitRate > 90) {
|
|
774
|
+
suggestions.push("Excellent cache performance - consider expanding caching rules");
|
|
775
|
+
}
|
|
776
|
+
if (this.cacheAnalytics.averageMissTime >
|
|
777
|
+
this.cacheAnalytics.averageHitTime * 10) {
|
|
778
|
+
suggestions.push("High miss penalty - consider cache warming strategies");
|
|
779
|
+
}
|
|
780
|
+
return suggestions;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Identify related content for prefetching
|
|
784
|
+
*/
|
|
785
|
+
identifyRelatedContent(path) {
|
|
786
|
+
const related = [];
|
|
787
|
+
// Simple related content identification
|
|
788
|
+
if (path.startsWith("/api/users/")) {
|
|
789
|
+
related.push("/api/users/profile", "/api/users/preferences");
|
|
790
|
+
}
|
|
791
|
+
if (path.startsWith("/api/products/")) {
|
|
792
|
+
related.push("/api/products/categories", "/api/products/featured");
|
|
793
|
+
}
|
|
794
|
+
return related;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Setup analytics cleanup
|
|
798
|
+
*/
|
|
799
|
+
setupAnalyticsCleanup() {
|
|
800
|
+
// Reset analytics every hour
|
|
801
|
+
setInterval(() => {
|
|
802
|
+
this.resetAnalytics();
|
|
803
|
+
}, 3600000); // 1 hour
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Setup dynamic TTL adjustment
|
|
807
|
+
*/
|
|
808
|
+
setupDynamicTTLAdjustment() {
|
|
809
|
+
// Analyze patterns every 10 minutes
|
|
810
|
+
setInterval(() => {
|
|
811
|
+
this.analyzeRequestPatterns();
|
|
812
|
+
}, 600000); // 10 minutes
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Reset analytics
|
|
816
|
+
*/
|
|
817
|
+
resetAnalytics() {
|
|
818
|
+
// Keep some historical data, reset counters
|
|
819
|
+
this.cacheAnalytics.totalRequests = 0;
|
|
820
|
+
this.cacheAnalytics.cacheableRequests = 0;
|
|
821
|
+
this.cacheAnalytics.cacheHits = 0;
|
|
822
|
+
this.cacheAnalytics.cacheMisses = 0;
|
|
823
|
+
this.cacheAnalytics.cacheSkips = 0;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Analyze request patterns for optimization
|
|
827
|
+
*/
|
|
828
|
+
analyzeRequestPatterns() {
|
|
829
|
+
const now = Date.now();
|
|
830
|
+
// Clean up old patterns
|
|
831
|
+
for (const [route, pattern] of this.requestPatterns.entries()) {
|
|
832
|
+
if (now - pattern.lastAccess > 3600000) {
|
|
833
|
+
// 1 hour
|
|
834
|
+
this.requestPatterns.delete(route);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export { SmartCachePlugin };
|
|
841
|
+
//# sourceMappingURL=SmartCachePlugin.js.map
|