kempo-server 1.6.0 → 1.6.2
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 +118 -0
- package/config-examples/development.config.json +24 -0
- package/config-examples/low-memory.config.json +23 -0
- package/config-examples/no-cache.config.json +13 -0
- package/config-examples/production.config.json +38 -0
- package/docs/.config.json.example +11 -1
- package/docs/api/_admin/cache/DELETE.js +28 -0
- package/docs/api/_admin/cache/GET.js +53 -0
- package/docs/caching.html +235 -0
- package/docs/configuration.html +76 -0
- package/docs/index.html +1 -0
- package/example-cache.config.json +45 -0
- package/package.json +1 -1
- package/src/defaultConfig.js +10 -0
- package/src/moduleCache.js +270 -0
- package/src/router.js +28 -4
- package/src/serveFile.js +35 -6
- package/tests/builtinMiddleware-cors.node-test.js +10 -10
- package/tests/builtinMiddleware.node-test.js +57 -58
- package/tests/cacheConfig.node-test.js +117 -0
- package/tests/defaultConfig.node-test.js +6 -7
- package/tests/example-middleware.node-test.js +16 -17
- package/tests/findFile.node-test.js +35 -35
- package/tests/getFiles.node-test.js +16 -16
- package/tests/getFlags.node-test.js +14 -14
- package/tests/index.node-test.js +24 -16
- package/tests/middlewareRunner.node-test.js +11 -11
- package/tests/moduleCache.node-test.js +279 -0
- package/tests/requestWrapper.node-test.js +28 -29
- package/tests/responseWrapper.node-test.js +55 -55
- package/tests/router-middleware.node-test.js +49 -37
- package/tests/router.node-test.js +96 -76
- package/utils/cache-demo.js +145 -0
- package/utils/cache-monitor.js +132 -0
package/docs/configuration.html
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
<li><a href="#routeFiles">routeFiles</a> - Files that should be treated as route handlers</li>
|
|
28
28
|
<li><a href="#noRescanPaths">noRescanPaths</a> - Paths that should not trigger file system rescans</li>
|
|
29
29
|
<li><a href="#maxRescanAttempts">maxRescanAttempts</a> - Maximum number of rescan attempts</li>
|
|
30
|
+
<li><a href="#cache">cache</a> - Module caching configuration</li>
|
|
30
31
|
<li><a href="#middleware">middleware</a> - Middleware configuration</li>
|
|
31
32
|
</ul>
|
|
32
33
|
|
|
@@ -91,6 +92,76 @@
|
|
|
91
92
|
<pre><code class="hljs json">{<br /> <span class="hljs-attr">"allowedMimes"</span>: {<br /> <span class="hljs-attr">"html"</span>: <span class="hljs-string">"text/html"</span>,<br /> <span class="hljs-attr">"css"</span>: <span class="hljs-string">"text/css"</span>,<br /> <span class="hljs-attr">"js"</span>: <span class="hljs-string">"application/javascript"</span>,<br /> <span class="hljs-attr">"json"</span>: <span class="hljs-string">"application/json"</span>,<br /> <span class="hljs-attr">"png"</span>: <span class="hljs-string">"image/png"</span>,<br /> <span class="hljs-attr">"jpg"</span>: <span class="hljs-string">"image/jpeg"</span><br /> },<br /> <span class="hljs-attr">"disallowedRegex"</span>: [<br /> <span class="hljs-string">"^/\\..*"</span>,<br /> <span class="hljs-string">"\\.env$"</span>,<br /> <span class="hljs-string">"\\.config$"</span>,<br /> <span class="hljs-string">"password"</span>,<br /> <span class="hljs-string">"node_modules"</span>,<br /> <span class="hljs-string">"\\.git"</span>,<br /> <span class="hljs-string">"\\.map$"</span><br /> ],<br /> <span class="hljs-attr">"middleware"</span>: {<br /> <span class="hljs-attr">"cors"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"origin"</span>: <span class="hljs-string">"https://yourdomain.com"</span><br /> },<br /> <span class="hljs-attr">"compression"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"threshold"</span>: <span class="hljs-number">1024</span><br /> },<br /> <span class="hljs-attr">"security"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"headers"</span>: {<br /> <span class="hljs-attr">"X-Content-Type-Options"</span>: <span class="hljs-string">"nosniff"</span>,<br /> <span class="hljs-attr">"X-Frame-Options"</span>: <span class="hljs-string">"DENY"</span>,<br /> <span class="hljs-attr">"X-XSS-Protection"</span>: <span class="hljs-string">"1; mode=block"</span>,<br /> <span class="hljs-attr">"Strict-Transport-Security"</span>: <span class="hljs-string">"max-age=31536000; includeSubDomains"</span><br /> }<br /> }<br /> }<br />}</code></pre>
|
|
92
93
|
<p>Use with: <code>kempo-server --root public --config .config.prod.json</code></p>
|
|
93
94
|
|
|
95
|
+
<h3 id="cache">cache</h3>
|
|
96
|
+
<p>Module caching configuration to improve performance by keeping compiled JavaScript modules in memory. The cache supports LRU eviction, TTL expiration, memory monitoring, and automatic file watching.</p>
|
|
97
|
+
<pre><code class="hljs json">{<br /> <span class="hljs-attr">"cache"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"maxSize"</span>: <span class="hljs-number">1000</span>,<br /> <span class="hljs-attr">"ttlMs"</span>: <span class="hljs-number">3600000</span>,<br /> <span class="hljs-attr">"maxMemoryUsageMB"</span>: <span class="hljs-number">500</span>,<br /> <span class="hljs-attr">"checkIntervalMs"</span>: <span class="hljs-number">30000</span>,<br /> <span class="hljs-attr">"fileWatching"</span>: <span class="hljs-literal">true</span><br /> }<br />}</code></pre>
|
|
98
|
+
|
|
99
|
+
<h4>Cache Configuration Options</h4>
|
|
100
|
+
<table>
|
|
101
|
+
<thead>
|
|
102
|
+
<tr>
|
|
103
|
+
<th>Option</th>
|
|
104
|
+
<th>Type</th>
|
|
105
|
+
<th>Default</th>
|
|
106
|
+
<th>Description</th>
|
|
107
|
+
</tr>
|
|
108
|
+
</thead>
|
|
109
|
+
<tbody>
|
|
110
|
+
<tr>
|
|
111
|
+
<td><code>enabled</code></td>
|
|
112
|
+
<td>boolean</td>
|
|
113
|
+
<td>true</td>
|
|
114
|
+
<td>Enable/disable module caching system</td>
|
|
115
|
+
</tr>
|
|
116
|
+
<tr>
|
|
117
|
+
<td><code>maxSize</code></td>
|
|
118
|
+
<td>number</td>
|
|
119
|
+
<td>1000</td>
|
|
120
|
+
<td>Maximum number of modules to cache (LRU eviction)</td>
|
|
121
|
+
</tr>
|
|
122
|
+
<tr>
|
|
123
|
+
<td><code>ttlMs</code></td>
|
|
124
|
+
<td>number</td>
|
|
125
|
+
<td>3600000</td>
|
|
126
|
+
<td>Time-to-live in milliseconds (default: 1 hour)</td>
|
|
127
|
+
</tr>
|
|
128
|
+
<tr>
|
|
129
|
+
<td><code>maxMemoryUsageMB</code></td>
|
|
130
|
+
<td>number</td>
|
|
131
|
+
<td>500</td>
|
|
132
|
+
<td>Maximum memory usage in MB before triggering cleanup</td>
|
|
133
|
+
</tr>
|
|
134
|
+
<tr>
|
|
135
|
+
<td><code>checkIntervalMs</code></td>
|
|
136
|
+
<td>number</td>
|
|
137
|
+
<td>30000</td>
|
|
138
|
+
<td>Memory check interval in milliseconds (default: 30 seconds)</td>
|
|
139
|
+
</tr>
|
|
140
|
+
<tr>
|
|
141
|
+
<td><code>fileWatching</code></td>
|
|
142
|
+
<td>boolean</td>
|
|
143
|
+
<td>true</td>
|
|
144
|
+
<td>Watch files for changes and auto-invalidate cache</td>
|
|
145
|
+
</tr>
|
|
146
|
+
</tbody>
|
|
147
|
+
</table>
|
|
148
|
+
|
|
149
|
+
<h4>Environment-Specific Cache Settings</h4>
|
|
150
|
+
<h5>Development (.config.dev.json)</h5>
|
|
151
|
+
<pre><code class="hljs json">{<br /> <span class="hljs-attr">"cache"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"maxSize"</span>: <span class="hljs-number">100</span>,<br /> <span class="hljs-attr">"ttlMs"</span>: <span class="hljs-number">300000</span>,<br /> <span class="hljs-attr">"fileWatching"</span>: <span class="hljs-literal">true</span><br /> }<br />}</code></pre>
|
|
152
|
+
|
|
153
|
+
<h5>Production (.config.prod.json)</h5>
|
|
154
|
+
<pre><code class="hljs json">{<br /> <span class="hljs-attr">"cache"</span>: {<br /> <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,<br /> <span class="hljs-attr">"maxSize"</span>: <span class="hljs-number">5000</span>,<br /> <span class="hljs-attr">"ttlMs"</span>: <span class="hljs-number">7200000</span>,<br /> <span class="hljs-attr">"maxMemoryUsageMB"</span>: <span class="hljs-number">1000</span>,<br /> <span class="hljs-attr">"fileWatching"</span>: <span class="hljs-literal">false</span><br /> }<br />}</code></pre>
|
|
155
|
+
|
|
156
|
+
<h4>Cache Monitoring</h4>
|
|
157
|
+
<p>Access cache statistics and management via admin endpoints:</p>
|
|
158
|
+
<ul>
|
|
159
|
+
<li><code>GET /_admin/cache</code> - View cache statistics (hits, misses, size, memory usage)</li>
|
|
160
|
+
<li><code>DELETE /_admin/cache</code> - Clear all cached modules</li>
|
|
161
|
+
</ul>
|
|
162
|
+
|
|
163
|
+
<p>For detailed cache usage and configuration examples, see the <a href="caching.html">Caching Guide</a>.</p>
|
|
164
|
+
|
|
94
165
|
<h3>Package.json Scripts</h3>
|
|
95
166
|
<p>Add environment-specific scripts to your <code>package.json</code>:</p>
|
|
96
167
|
<pre><code class="hljs json">{<br /> <span class="hljs-attr">"scripts"</span>: {<br /> <span class="hljs-attr">"start"</span>: <span class="hljs-string">"kempo-server --root public"</span>,<br /> <span class="hljs-attr">"start:dev"</span>: <span class="hljs-string">"kempo-server --root public --config .config.dev.json --verbose"</span>,<br /> <span class="hljs-attr">"start:staging"</span>: <span class="hljs-string">"kempo-server --root public --config .config.staging.json"</span>,<br /> <span class="hljs-attr">"start:prod"</span>: <span class="hljs-string">"kempo-server --root public --config .config.prod.json"</span><br /> }<br />}</code></pre>
|
|
@@ -107,10 +178,12 @@
|
|
|
107
178
|
|
|
108
179
|
<h3>Performance Optimization</h3>
|
|
109
180
|
<ul>
|
|
181
|
+
<li>Enable module caching with appropriate <code>maxSize</code> and <code>ttlMs</code> settings</li>
|
|
110
182
|
<li>Use <code>noRescanPaths</code> for static assets to improve performance</li>
|
|
111
183
|
<li>Enable compression for larger files</li>
|
|
112
184
|
<li>Use custom routes to serve files from CDN or optimized locations</li>
|
|
113
185
|
<li>Limit <code>maxRescanAttempts</code> to prevent excessive file system scanning</li>
|
|
186
|
+
<li>Configure <code>maxMemoryUsageMB</code> to prevent memory issues in production</li>
|
|
114
187
|
</ul>
|
|
115
188
|
|
|
116
189
|
<h3>Development vs Production</h3>
|
|
@@ -119,6 +192,9 @@
|
|
|
119
192
|
<li>Use relaxed CORS in development, strict in production</li>
|
|
120
193
|
<li>Enable compression in production for better performance</li>
|
|
121
194
|
<li>Add more security headers in production</li>
|
|
195
|
+
<li>Use smaller cache sizes in development, larger in production</li>
|
|
196
|
+
<li>Enable file watching in development, disable in production for stability</li>
|
|
197
|
+
<li>Use shorter TTL in development for faster iteration</li>
|
|
122
198
|
</ul>
|
|
123
199
|
</main>
|
|
124
200
|
<div style="height:25vh"></div>
|
package/docs/index.html
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cache": {
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"maxSize": 200,
|
|
5
|
+
"maxMemoryMB": 75,
|
|
6
|
+
"ttlMs": 600000,
|
|
7
|
+
"maxHeapUsagePercent": 75,
|
|
8
|
+
"memoryCheckInterval": 15000,
|
|
9
|
+
"watchFiles": true,
|
|
10
|
+
"enableMemoryMonitoring": true,
|
|
11
|
+
"production": {
|
|
12
|
+
"maxSize": 1000,
|
|
13
|
+
"maxMemoryMB": 200,
|
|
14
|
+
"ttlMs": 3600000,
|
|
15
|
+
"maxHeapUsagePercent": 85,
|
|
16
|
+
"memoryCheckInterval": 60000,
|
|
17
|
+
"watchFiles": false
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"middleware": {
|
|
21
|
+
"cors": {
|
|
22
|
+
"enabled": true,
|
|
23
|
+
"origin": "*",
|
|
24
|
+
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
25
|
+
"headers": ["Content-Type", "Authorization"]
|
|
26
|
+
},
|
|
27
|
+
"compression": {
|
|
28
|
+
"enabled": true,
|
|
29
|
+
"threshold": 1024
|
|
30
|
+
},
|
|
31
|
+
"security": {
|
|
32
|
+
"enabled": true,
|
|
33
|
+
"headers": {
|
|
34
|
+
"X-Content-Type-Options": "nosniff",
|
|
35
|
+
"X-Frame-Options": "DENY",
|
|
36
|
+
"X-XSS-Protection": "1; mode=block"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"logging": {
|
|
40
|
+
"enabled": true,
|
|
41
|
+
"includeUserAgent": false,
|
|
42
|
+
"includeResponseTime": true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
CHANGED
package/src/defaultConfig.js
CHANGED
|
@@ -126,5 +126,15 @@ export default {
|
|
|
126
126
|
custom: [
|
|
127
127
|
// Example: "./middleware/auth.js"
|
|
128
128
|
]
|
|
129
|
+
},
|
|
130
|
+
cache: {
|
|
131
|
+
enabled: false, // Disabled by default - opt-in for performance
|
|
132
|
+
maxSize: 100, // Maximum number of cached modules
|
|
133
|
+
maxMemoryMB: 50, // Memory limit for cache in MB
|
|
134
|
+
ttlMs: 300000, // Cache TTL (5 minutes default)
|
|
135
|
+
maxHeapUsagePercent: 70, // Clear cache if heap usage exceeds this
|
|
136
|
+
memoryCheckInterval: 30000, // Memory check interval (30 seconds)
|
|
137
|
+
watchFiles: true, // Auto-invalidate on file changes
|
|
138
|
+
enableMemoryMonitoring: true // Enable automatic memory monitoring
|
|
129
139
|
}
|
|
130
140
|
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { watch } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Hybrid Module Cache System
|
|
6
|
+
Combines LRU, time-based expiration, and memory monitoring
|
|
7
|
+
*/
|
|
8
|
+
export default class ModuleCache {
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
this.cache = new Map();
|
|
11
|
+
this.watchers = new Map();
|
|
12
|
+
|
|
13
|
+
// Configuration
|
|
14
|
+
this.maxSize = config.maxSize || 100;
|
|
15
|
+
this.maxMemoryMB = config.maxMemoryMB || 50;
|
|
16
|
+
this.ttlMs = config.ttlMs || 300000; // 5 minutes default
|
|
17
|
+
this.maxHeapUsagePercent = config.maxHeapUsagePercent || 70;
|
|
18
|
+
this.memoryCheckInterval = config.memoryCheckInterval || 30000; // 30 seconds
|
|
19
|
+
this.watchFiles = config.watchFiles !== false; // Default to true
|
|
20
|
+
|
|
21
|
+
// State tracking
|
|
22
|
+
this.currentMemoryMB = 0;
|
|
23
|
+
this.stats = {
|
|
24
|
+
hits: 0,
|
|
25
|
+
misses: 0,
|
|
26
|
+
evictions: 0,
|
|
27
|
+
fileChanges: 0
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Start memory monitoring
|
|
31
|
+
if (config.enableMemoryMonitoring !== false) {
|
|
32
|
+
this.startMemoryMonitoring();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
Lifecycle Management
|
|
38
|
+
*/
|
|
39
|
+
startMemoryMonitoring() {
|
|
40
|
+
this.memoryTimer = setInterval(() => {
|
|
41
|
+
const usage = process.memoryUsage();
|
|
42
|
+
const heapPercent = (usage.heapUsed / usage.heapTotal) * 100;
|
|
43
|
+
|
|
44
|
+
if (heapPercent > this.maxHeapUsagePercent) {
|
|
45
|
+
const clearedCount = this.clearExpiredEntries();
|
|
46
|
+
this.stats.evictions += clearedCount;
|
|
47
|
+
|
|
48
|
+
// If still over limit, clear oldest entries
|
|
49
|
+
while (this.cache.size > 0 && heapPercent > this.maxHeapUsagePercent) {
|
|
50
|
+
this.evictOldest();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}, this.memoryCheckInterval);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
destroy() {
|
|
57
|
+
if (this.memoryTimer) {
|
|
58
|
+
clearInterval(this.memoryTimer);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Clean up file watchers
|
|
62
|
+
for (const watcher of this.watchers.values()) {
|
|
63
|
+
watcher.close();
|
|
64
|
+
}
|
|
65
|
+
this.watchers.clear();
|
|
66
|
+
this.cache.clear();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
Cache Operations
|
|
71
|
+
*/
|
|
72
|
+
get(filePath, stats) {
|
|
73
|
+
const entry = this.cache.get(filePath);
|
|
74
|
+
if (!entry) {
|
|
75
|
+
this.stats.misses++;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
|
|
81
|
+
// Check if expired by time
|
|
82
|
+
if (now - entry.timestamp > this.ttlMs) {
|
|
83
|
+
this.delete(filePath);
|
|
84
|
+
this.stats.misses++;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check if file was modified
|
|
89
|
+
if (entry.mtime < stats.mtime) {
|
|
90
|
+
this.delete(filePath);
|
|
91
|
+
this.stats.misses++;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Move to end (most recently used)
|
|
96
|
+
this.cache.delete(filePath);
|
|
97
|
+
this.cache.set(filePath, entry);
|
|
98
|
+
|
|
99
|
+
this.stats.hits++;
|
|
100
|
+
return entry.module;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
set(filePath, module, stats, estimatedSizeKB = 1) {
|
|
104
|
+
// Evict entries if we'd exceed limits
|
|
105
|
+
const sizeInMB = estimatedSizeKB / 1024;
|
|
106
|
+
|
|
107
|
+
while (
|
|
108
|
+
this.cache.size >= this.maxSize ||
|
|
109
|
+
this.currentMemoryMB + sizeInMB > this.maxMemoryMB
|
|
110
|
+
) {
|
|
111
|
+
this.evictOldest();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Create cache entry
|
|
115
|
+
const entry = {
|
|
116
|
+
module,
|
|
117
|
+
mtime: stats.mtime,
|
|
118
|
+
timestamp: Date.now(),
|
|
119
|
+
sizeKB: estimatedSizeKB,
|
|
120
|
+
filePath
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
this.cache.set(filePath, entry);
|
|
124
|
+
this.currentMemoryMB += sizeInMB;
|
|
125
|
+
|
|
126
|
+
// Set up file watcher if enabled
|
|
127
|
+
if (this.watchFiles) {
|
|
128
|
+
this.setupFileWatcher(filePath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
delete(filePath) {
|
|
133
|
+
const entry = this.cache.get(filePath);
|
|
134
|
+
if (entry) {
|
|
135
|
+
this.cache.delete(filePath);
|
|
136
|
+
this.currentMemoryMB -= entry.sizeKB / 1024;
|
|
137
|
+
|
|
138
|
+
// Remove file watcher
|
|
139
|
+
const watcher = this.watchers.get(filePath);
|
|
140
|
+
if (watcher) {
|
|
141
|
+
watcher.close();
|
|
142
|
+
this.watchers.delete(filePath);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
clear() {
|
|
151
|
+
const size = this.cache.size;
|
|
152
|
+
this.cache.clear();
|
|
153
|
+
this.currentMemoryMB = 0;
|
|
154
|
+
|
|
155
|
+
// Clean up all watchers
|
|
156
|
+
for (const watcher of this.watchers.values()) {
|
|
157
|
+
watcher.close();
|
|
158
|
+
}
|
|
159
|
+
this.watchers.clear();
|
|
160
|
+
|
|
161
|
+
this.stats.evictions += size;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/*
|
|
165
|
+
Cache Management
|
|
166
|
+
*/
|
|
167
|
+
evictOldest() {
|
|
168
|
+
if (this.cache.size === 0) return;
|
|
169
|
+
|
|
170
|
+
const [oldestKey, oldestEntry] = this.cache.entries().next().value;
|
|
171
|
+
this.delete(oldestKey);
|
|
172
|
+
this.stats.evictions++;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
clearExpiredEntries() {
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
let clearedCount = 0;
|
|
178
|
+
|
|
179
|
+
for (const [filePath, entry] of this.cache.entries()) {
|
|
180
|
+
if (now - entry.timestamp > this.ttlMs) {
|
|
181
|
+
this.delete(filePath);
|
|
182
|
+
clearedCount++;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return clearedCount;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/*
|
|
190
|
+
File Watching
|
|
191
|
+
*/
|
|
192
|
+
setupFileWatcher(filePath) {
|
|
193
|
+
// Don't create duplicate watchers
|
|
194
|
+
if (this.watchers.has(filePath)) return;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const watcher = watch(filePath, (eventType) => {
|
|
198
|
+
if (eventType === 'change') {
|
|
199
|
+
this.delete(filePath);
|
|
200
|
+
this.stats.fileChanges++;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Handle watcher errors gracefully
|
|
205
|
+
watcher.on('error', (error) => {
|
|
206
|
+
// File might have been deleted or moved
|
|
207
|
+
this.delete(filePath);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
this.watchers.set(filePath, watcher);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
// File watching failed, but cache can still work
|
|
213
|
+
console.warn(`Could not watch file ${filePath}: ${error.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/*
|
|
218
|
+
Utilities and Stats
|
|
219
|
+
*/
|
|
220
|
+
getStats() {
|
|
221
|
+
const memoryUsage = process.memoryUsage();
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
cache: {
|
|
225
|
+
size: this.cache.size,
|
|
226
|
+
maxSize: this.maxSize,
|
|
227
|
+
memoryUsageMB: Math.round(this.currentMemoryMB * 100) / 100,
|
|
228
|
+
maxMemoryMB: this.maxMemoryMB,
|
|
229
|
+
watchersActive: this.watchers.size
|
|
230
|
+
},
|
|
231
|
+
stats: { ...this.stats },
|
|
232
|
+
memory: {
|
|
233
|
+
heapUsedMB: Math.round(memoryUsage.heapUsed / 1024 / 1024 * 100) / 100,
|
|
234
|
+
heapTotalMB: Math.round(memoryUsage.heapTotal / 1024 / 1024 * 100) / 100,
|
|
235
|
+
heapUsagePercent: Math.round((memoryUsage.heapUsed / memoryUsage.heapTotal) * 100),
|
|
236
|
+
rssMB: Math.round(memoryUsage.rss / 1024 / 1024 * 100) / 100
|
|
237
|
+
},
|
|
238
|
+
config: {
|
|
239
|
+
ttlMs: this.ttlMs,
|
|
240
|
+
maxHeapUsagePercent: this.maxHeapUsagePercent,
|
|
241
|
+
watchFiles: this.watchFiles
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
getHitRate() {
|
|
247
|
+
const total = this.stats.hits + this.stats.misses;
|
|
248
|
+
return total === 0 ? 0 : Math.round((this.stats.hits / total) * 100);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/*
|
|
252
|
+
Development Helpers
|
|
253
|
+
*/
|
|
254
|
+
logStats(log) {
|
|
255
|
+
const stats = this.getStats();
|
|
256
|
+
log(`Cache Stats: ${stats.cache.size}/${stats.cache.maxSize} entries, ` +
|
|
257
|
+
`${stats.cache.memoryUsageMB}/${stats.cache.maxMemoryMB}MB, ` +
|
|
258
|
+
`${this.getHitRate()}% hit rate`, 2);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// For debugging - list all cached files
|
|
262
|
+
getCachedFiles() {
|
|
263
|
+
return Array.from(this.cache.keys()).map(filePath => ({
|
|
264
|
+
path: filePath,
|
|
265
|
+
relativePath: path.relative(process.cwd(), filePath),
|
|
266
|
+
age: Date.now() - this.cache.get(filePath).timestamp,
|
|
267
|
+
sizeKB: this.cache.get(filePath).sizeKB
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
}
|
package/src/router.js
CHANGED
|
@@ -6,6 +6,7 @@ import getFiles from './getFiles.js';
|
|
|
6
6
|
import findFile from './findFile.js';
|
|
7
7
|
import serveFile from './serveFile.js';
|
|
8
8
|
import MiddlewareRunner from './middlewareRunner.js';
|
|
9
|
+
import ModuleCache from './moduleCache.js';
|
|
9
10
|
import {
|
|
10
11
|
corsMiddleware,
|
|
11
12
|
compressionMiddleware,
|
|
@@ -44,6 +45,10 @@ export default async (flags, log) => {
|
|
|
44
45
|
customRoutes: {
|
|
45
46
|
...defaultConfig.customRoutes,
|
|
46
47
|
...userConfig.customRoutes
|
|
48
|
+
},
|
|
49
|
+
cache: {
|
|
50
|
+
...defaultConfig.cache,
|
|
51
|
+
...userConfig.cache
|
|
47
52
|
}
|
|
48
53
|
};
|
|
49
54
|
log('User config loaded and merged with defaults', 2);
|
|
@@ -115,6 +120,16 @@ export default async (flags, log) => {
|
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
/*
|
|
124
|
+
Initialize Module Cache
|
|
125
|
+
*/
|
|
126
|
+
let moduleCache = null;
|
|
127
|
+
if (config.cache?.enabled) {
|
|
128
|
+
moduleCache = new ModuleCache(config.cache);
|
|
129
|
+
log(`Module cache initialized: ${config.cache.maxSize} max modules, ` +
|
|
130
|
+
`${config.cache.maxMemoryMB}MB limit, ${config.cache.ttlMs}ms TTL`, 2);
|
|
131
|
+
}
|
|
132
|
+
|
|
118
133
|
// Process custom routes - resolve paths and validate files exist
|
|
119
134
|
const customRoutes = new Map();
|
|
120
135
|
const wildcardRoutes = new Map();
|
|
@@ -227,7 +242,7 @@ export default async (flags, log) => {
|
|
|
227
242
|
return newAttempts;
|
|
228
243
|
};
|
|
229
244
|
|
|
230
|
-
|
|
245
|
+
const requestHandler = async (req, res) => {
|
|
231
246
|
await middlewareRunner.run(req, res, async () => {
|
|
232
247
|
const requestPath = req.url.split('?')[0];
|
|
233
248
|
log(`${req.method} ${requestPath}`, 0);
|
|
@@ -278,7 +293,7 @@ export default async (flags, log) => {
|
|
|
278
293
|
}
|
|
279
294
|
|
|
280
295
|
// Try to serve the file normally
|
|
281
|
-
const served = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log);
|
|
296
|
+
const served = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log, moduleCache);
|
|
282
297
|
|
|
283
298
|
// If not served and scan flag is enabled, try rescanning once (with blacklist check)
|
|
284
299
|
if (!served && flags.scan && !shouldSkipRescan(requestPath)) {
|
|
@@ -288,7 +303,7 @@ export default async (flags, log) => {
|
|
|
288
303
|
log(`Rescan found ${files.length} files`, 2);
|
|
289
304
|
|
|
290
305
|
// Try to serve again after rescan
|
|
291
|
-
const reserved = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log);
|
|
306
|
+
const reserved = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log, moduleCache);
|
|
292
307
|
|
|
293
308
|
if (!reserved) {
|
|
294
309
|
log(`404 - File not found after rescan: ${requestPath}`, 1);
|
|
@@ -305,5 +320,14 @@ export default async (flags, log) => {
|
|
|
305
320
|
res.end('Not Found');
|
|
306
321
|
}
|
|
307
322
|
});
|
|
308
|
-
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Return handler with cache instance for external access
|
|
326
|
+
const handler = requestHandler;
|
|
327
|
+
handler.moduleCache = moduleCache;
|
|
328
|
+
handler.getStats = () => moduleCache?.getStats() || null;
|
|
329
|
+
handler.logCacheStats = () => moduleCache?.logStats(log);
|
|
330
|
+
handler.clearCache = () => moduleCache?.clear();
|
|
331
|
+
|
|
332
|
+
return handler;
|
|
309
333
|
}
|
package/src/serveFile.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { readFile } from 'fs/promises';
|
|
2
|
+
import { readFile, stat } from 'fs/promises';
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
4
|
import findFile from './findFile.js';
|
|
5
5
|
import createRequestWrapper from './requestWrapper.js';
|
|
6
6
|
import createResponseWrapper from './responseWrapper.js';
|
|
7
7
|
|
|
8
|
-
export default async (files, rootPath, requestPath, method, config, req, res, log) => {
|
|
8
|
+
export default async (files, rootPath, requestPath, method, config, req, res, log, moduleCache = null) => {
|
|
9
9
|
log(`Attempting to serve: ${requestPath}`, 3);
|
|
10
10
|
const [file, params] = await findFile(files, rootPath, requestPath, method, log);
|
|
11
11
|
|
|
@@ -21,10 +21,34 @@ export default async (files, rootPath, requestPath, method, config, req, res, lo
|
|
|
21
21
|
if (config.routeFiles.includes(fileName)) {
|
|
22
22
|
log(`Executing route file: ${fileName}`, 2);
|
|
23
23
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
let module;
|
|
25
|
+
|
|
26
|
+
if (moduleCache && config.cache?.enabled) {
|
|
27
|
+
// Get file stats for cache validation
|
|
28
|
+
const fileStats = await stat(file);
|
|
29
|
+
|
|
30
|
+
// Try to get from cache first
|
|
31
|
+
module = moduleCache.get(file, fileStats);
|
|
32
|
+
|
|
33
|
+
if (!module) {
|
|
34
|
+
// Cache miss - load module
|
|
35
|
+
const fileUrl = pathToFileURL(file).href + `?t=${Date.now()}`;
|
|
36
|
+
log(`Loading module from: ${fileUrl}`, 3);
|
|
37
|
+
module = await import(fileUrl);
|
|
38
|
+
|
|
39
|
+
// Estimate module size (rough approximation based on file size)
|
|
40
|
+
const estimatedSizeKB = fileStats.size / 1024;
|
|
41
|
+
moduleCache.set(file, module, fileStats, estimatedSizeKB);
|
|
42
|
+
log(`Cached module: ${fileName} (${estimatedSizeKB.toFixed(1)}KB)`, 3);
|
|
43
|
+
} else {
|
|
44
|
+
log(`Using cached module: ${fileName}`, 3);
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
// No caching - load fresh each time
|
|
48
|
+
const fileUrl = pathToFileURL(file).href + `?t=${Date.now()}`;
|
|
49
|
+
log(`Loading module from: ${fileUrl}`, 3);
|
|
50
|
+
module = await import(fileUrl);
|
|
51
|
+
}
|
|
28
52
|
|
|
29
53
|
// Execute the default export function
|
|
30
54
|
if (typeof module.default === 'function') {
|
|
@@ -34,6 +58,11 @@ export default async (files, rootPath, requestPath, method, config, req, res, lo
|
|
|
34
58
|
const enhancedRequest = createRequestWrapper(req, params);
|
|
35
59
|
const enhancedResponse = createResponseWrapper(res);
|
|
36
60
|
|
|
61
|
+
// Make module cache accessible for admin endpoints
|
|
62
|
+
if (moduleCache) {
|
|
63
|
+
enhancedRequest._kempoCache = moduleCache;
|
|
64
|
+
}
|
|
65
|
+
|
|
37
66
|
await module.default(enhancedRequest, enhancedResponse);
|
|
38
67
|
log(`Route executed successfully: ${fileName}`, 2);
|
|
39
68
|
return true; // Successfully served
|
|
@@ -3,15 +3,15 @@ import {createMockReq, createMockRes} from './test-utils.js';
|
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
'cors origin array and non-OPTIONS continues to next': async ({pass, fail}) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
const res = createMockRes();
|
|
7
|
+
const mw = corsMiddleware({origin: ['http://a','http://b'], methods: ['GET'], headers: ['X']});
|
|
8
|
+
const req = createMockReq({method: 'GET', headers: {origin: 'http://b'}});
|
|
9
|
+
let called = false;
|
|
10
|
+
await mw(req, res, async () => { called = true; });
|
|
11
|
+
|
|
12
|
+
if(!called) return fail('next not called');
|
|
13
|
+
if(res.getHeader('Access-Control-Allow-Origin') !== 'http://b') return fail('allowed origin');
|
|
14
|
+
|
|
15
|
+
pass('cors array');
|
|
16
16
|
}
|
|
17
17
|
};
|