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.
Files changed (34) hide show
  1. package/README.md +118 -0
  2. package/config-examples/development.config.json +24 -0
  3. package/config-examples/low-memory.config.json +23 -0
  4. package/config-examples/no-cache.config.json +13 -0
  5. package/config-examples/production.config.json +38 -0
  6. package/docs/.config.json.example +11 -1
  7. package/docs/api/_admin/cache/DELETE.js +28 -0
  8. package/docs/api/_admin/cache/GET.js +53 -0
  9. package/docs/caching.html +235 -0
  10. package/docs/configuration.html +76 -0
  11. package/docs/index.html +1 -0
  12. package/example-cache.config.json +45 -0
  13. package/package.json +1 -1
  14. package/src/defaultConfig.js +10 -0
  15. package/src/moduleCache.js +270 -0
  16. package/src/router.js +28 -4
  17. package/src/serveFile.js +35 -6
  18. package/tests/builtinMiddleware-cors.node-test.js +10 -10
  19. package/tests/builtinMiddleware.node-test.js +57 -58
  20. package/tests/cacheConfig.node-test.js +117 -0
  21. package/tests/defaultConfig.node-test.js +6 -7
  22. package/tests/example-middleware.node-test.js +16 -17
  23. package/tests/findFile.node-test.js +35 -35
  24. package/tests/getFiles.node-test.js +16 -16
  25. package/tests/getFlags.node-test.js +14 -14
  26. package/tests/index.node-test.js +24 -16
  27. package/tests/middlewareRunner.node-test.js +11 -11
  28. package/tests/moduleCache.node-test.js +279 -0
  29. package/tests/requestWrapper.node-test.js +28 -29
  30. package/tests/responseWrapper.node-test.js +55 -55
  31. package/tests/router-middleware.node-test.js +49 -37
  32. package/tests/router.node-test.js +96 -76
  33. package/utils/cache-demo.js +145 -0
  34. package/utils/cache-monitor.js +132 -0
@@ -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
@@ -34,6 +34,7 @@
34
34
  <ul>
35
35
  <li><a href="configuration.html">Configuration</a></li>
36
36
  <li><a href="middleware.html">Middleware</a></li>
37
+ <li><a href="caching.html">Module Caching</a></li>
37
38
  <li><a href="examples.html">Examples & Demos</a></li>
38
39
  </ul>
39
40
  </div>
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kempo-server",
3
3
  "type": "module",
4
- "version": "1.6.0",
4
+ "version": "1.6.2",
5
5
  "description": "A lightweight, zero-dependency, file based routing server.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -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
- return async (req, res) => {
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
- // Load the file as a module
25
- const fileUrl = pathToFileURL(file).href;
26
- log(`Loading module from: ${fileUrl}`, 3);
27
- const module = await import(fileUrl);
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
- try {
7
- const res = createMockRes();
8
- const mw = corsMiddleware({origin: ['http://a','http://b'], methods: ['GET'], headers: ['X']});
9
- const req = createMockReq({method: 'GET', headers: {origin: 'http://b'}});
10
- let called = false;
11
- await mw(req, res, async () => { called = true; });
12
- if(!called) return fail('next not called');
13
- if(res.getHeader('Access-Control-Allow-Origin') !== 'http://b') return fail('allowed origin');
14
- pass('cors array');
15
- } catch(e){ fail(e.message); }
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
  };