kempo-server 1.6.0 → 1.6.1

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.
@@ -0,0 +1,107 @@
1
+ import defaultConfig from '../src/defaultConfig.js';
2
+ import path from 'path';
3
+ import { writeFile, mkdir, rm } from 'fs/promises';
4
+
5
+ /*
6
+ Test Utilities
7
+ */
8
+ const createTempConfig = async (config) => {
9
+ const tempDir = path.join(process.cwd(), 'tests', 'temp-config-test');
10
+ await mkdir(tempDir, { recursive: true });
11
+
12
+ const configPath = path.join(tempDir, 'test.config.json');
13
+ await writeFile(configPath, JSON.stringify(config, null, 2));
14
+
15
+ return { tempDir, configPath };
16
+ };
17
+
18
+ const cleanupTempConfig = async (tempDir) => {
19
+ try {
20
+ await rm(tempDir, { recursive: true });
21
+ } catch(e) {
22
+ // Directory might not exist
23
+ }
24
+ };
25
+
26
+ /*
27
+ Lifecycle Callbacks
28
+ */
29
+ export const afterAll = async (log) => {
30
+ log('Cleaning up cache configuration test environment...');
31
+ const tempDir = path.join(process.cwd(), 'tests', 'temp-config-test');
32
+ await cleanupTempConfig(tempDir);
33
+ };
34
+
35
+ /*
36
+ Cache Configuration Tests
37
+ */
38
+ export default {
39
+ 'default cache configuration includes all required fields': async ({pass, fail, log}) => {
40
+ try {
41
+ const cache = defaultConfig.cache;
42
+
43
+ if(!cache) throw new Error('cache config missing from defaults');
44
+ if(typeof cache.enabled !== 'boolean') throw new Error('enabled should be boolean');
45
+ if(typeof cache.maxSize !== 'number') throw new Error('maxSize should be number');
46
+ if(typeof cache.maxMemoryMB !== 'number') throw new Error('maxMemoryMB should be number');
47
+ if(typeof cache.ttlMs !== 'number') throw new Error('ttlMs should be number');
48
+ if(typeof cache.maxHeapUsagePercent !== 'number') throw new Error('maxHeapUsagePercent should be number');
49
+ if(typeof cache.memoryCheckInterval !== 'number') throw new Error('memoryCheckInterval should be number');
50
+ if(typeof cache.watchFiles !== 'boolean') throw new Error('watchFiles should be boolean');
51
+ if(typeof cache.enableMemoryMonitoring !== 'boolean') throw new Error('enableMemoryMonitoring should be boolean');
52
+
53
+ log('✓ Default cache configuration structure valid');
54
+ pass('Default cache configuration verified');
55
+ } catch(e){ fail(e.message); }
56
+ },
57
+
58
+ 'cache can be completely disabled': async ({pass, fail, log}) => {
59
+ try {
60
+ const { tempDir, configPath } = await createTempConfig({
61
+ cache: { enabled: false }
62
+ });
63
+
64
+ // Import router to test config loading
65
+ const router = await import('../src/router.js');
66
+ const flags = { root: 'docs', config: configPath };
67
+
68
+ const handler = await router.default(flags, () => {}); // Empty log function
69
+
70
+ if(handler.moduleCache !== null) throw new Error('cache should be null when disabled');
71
+
72
+ await cleanupTempConfig(tempDir);
73
+ log('✓ Cache properly disabled via configuration');
74
+ pass('Cache disable configuration verified');
75
+ } catch(e){ fail(e.message); }
76
+ },
77
+
78
+ 'custom cache limits are applied correctly': async ({pass, fail, log}) => {
79
+ try {
80
+ const { tempDir, configPath } = await createTempConfig({
81
+ cache: {
82
+ enabled: true,
83
+ maxSize: 25,
84
+ maxMemoryMB: 10,
85
+ ttlMs: 120000
86
+ }
87
+ });
88
+
89
+ const router = await import('../src/router.js');
90
+ const flags = { root: 'docs', config: configPath };
91
+
92
+ const handler = await router.default(flags, () => {});
93
+
94
+ if(!handler.moduleCache) throw new Error('cache should be enabled');
95
+
96
+ const stats = handler.getStats();
97
+ if(stats.cache.maxSize !== 25) throw new Error('maxSize not applied');
98
+ if(stats.cache.maxMemoryMB !== 10) throw new Error('maxMemoryMB not applied');
99
+ if(stats.config.ttlMs !== 120000) throw new Error('ttlMs not applied');
100
+
101
+ handler.moduleCache.destroy();
102
+ await cleanupTempConfig(tempDir);
103
+ log('✓ Custom cache limits applied correctly');
104
+ pass('Custom cache configuration verified');
105
+ } catch(e){ fail(e.message); }
106
+ }
107
+ };
@@ -0,0 +1,274 @@
1
+ import ModuleCache from '../src/moduleCache.js';
2
+ import path from 'path';
3
+ import { writeFile, unlink, stat, mkdir, rm } from 'fs/promises';
4
+
5
+ /*
6
+ Test Utilities
7
+ */
8
+ const activeCaches = [];
9
+
10
+ const createTestCache = (config = {}) => {
11
+ const cache = new ModuleCache({
12
+ enableMemoryMonitoring: false, // Always disable to prevent hanging
13
+ watchFiles: false, // Default to false for most tests
14
+ ...config
15
+ });
16
+ activeCaches.push(cache);
17
+ return cache;
18
+ };
19
+ const createTempDir = async () => {
20
+ const tempDir = path.join(process.cwd(), 'tests', 'temp-cache-test');
21
+ try {
22
+ await mkdir(tempDir, { recursive: true });
23
+ } catch(e) {
24
+ // Directory might already exist
25
+ }
26
+ return tempDir;
27
+ };
28
+
29
+ const cleanupTempDir = async (tempDir) => {
30
+ try {
31
+ await rm(tempDir, { recursive: true });
32
+ } catch(e) {
33
+ // Directory might not exist
34
+ }
35
+ };
36
+
37
+ /*
38
+ Lifecycle Callbacks
39
+ */
40
+ export const beforeAll = async (log) => {
41
+ log('Setting up module cache test environment...');
42
+ };
43
+
44
+ export const afterAll = async (log) => {
45
+ log('Starting cleanup of module cache test environment...');
46
+
47
+ log(`Found ${activeCaches.length} active caches to destroy`);
48
+ // Destroy all active caches to clean up timers and watchers
49
+ for(let i = 0; i < activeCaches.length; i++) {
50
+ log(`Destroying cache ${i + 1}/${activeCaches.length}`);
51
+ activeCaches[i].destroy();
52
+ }
53
+ activeCaches.length = 0;
54
+ log('All caches destroyed');
55
+
56
+ // Give file watchers extra time to fully close
57
+ log('Waiting for file watchers to close...');
58
+ await new Promise(resolve => setTimeout(resolve, 500));
59
+ log('File watcher wait complete');
60
+
61
+ const tempDir = path.join(process.cwd(), 'tests', 'temp-cache-test');
62
+ log('Cleaning up temp directory...');
63
+ await cleanupTempDir(tempDir);
64
+ log('Module cache test cleanup complete');
65
+ };
66
+
67
+ /*
68
+ Module Cache Tests
69
+ */
70
+ export default {
71
+ 'basic LRU functionality works correctly': async ({pass, fail, log}) => {
72
+ try {
73
+ const cache = createTestCache({
74
+ maxSize: 2,
75
+ maxMemoryMB: 10,
76
+ ttlMs: 60000,
77
+ watchFiles: false
78
+ });
79
+
80
+ const mockStats1 = { mtime: new Date('2023-01-01') };
81
+ const mockStats2 = { mtime: new Date('2023-01-02') };
82
+ const mockStats3 = { mtime: new Date('2023-01-03') };
83
+
84
+ cache.set('/test1.js', { default: () => 'test1' }, mockStats1, 1);
85
+ if(cache.cache.size !== 1) throw new Error('size should be 1');
86
+
87
+ cache.set('/test2.js', { default: () => 'test2' }, mockStats2, 1);
88
+ if(cache.cache.size !== 2) throw new Error('size should be 2');
89
+
90
+ cache.set('/test3.js', { default: () => 'test3' }, mockStats3, 1);
91
+ if(cache.cache.size !== 2) throw new Error('size should still be 2');
92
+ if(cache.get('/test1.js', mockStats1) !== null) throw new Error('oldest should be evicted');
93
+
94
+ cache.destroy();
95
+ log('✓ LRU eviction working correctly');
96
+ pass('LRU functionality verified');
97
+ } catch(e){
98
+ cache?.destroy();
99
+ fail(e.message);
100
+ }
101
+ },
102
+
103
+ 'TTL expiration invalidates entries': async ({pass, fail, log}) => {
104
+ try {
105
+ const cache = createTestCache({
106
+ maxSize: 10,
107
+ ttlMs: 50,
108
+ watchFiles: false
109
+ });
110
+
111
+ const mockStats = { mtime: new Date('2023-01-01') };
112
+ cache.set('/test.js', { default: () => 'test' }, mockStats, 1);
113
+
114
+ if(cache.get('/test.js', mockStats) === null) throw new Error('should be available immediately');
115
+
116
+ await new Promise(resolve => setTimeout(resolve, 100));
117
+
118
+ if(cache.get('/test.js', mockStats) !== null) throw new Error('should be expired');
119
+
120
+ cache.destroy();
121
+ log('✓ TTL expiration working');
122
+ pass('TTL expiration verified');
123
+ } catch(e){
124
+ cache?.destroy();
125
+ fail(e.message);
126
+ }
127
+ },
128
+
129
+ 'file modification invalidates cache entry': async ({pass, fail, log}) => {
130
+ try {
131
+ const cache = createTestCache({
132
+ maxSize: 10,
133
+ ttlMs: 60000,
134
+ watchFiles: false
135
+ });
136
+
137
+ const oldStats = { mtime: new Date('2023-01-01') };
138
+ const newStats = { mtime: new Date('2023-01-02') };
139
+
140
+ cache.set('/test.js', { default: () => 'test' }, oldStats, 1);
141
+
142
+ if(cache.get('/test.js', oldStats) === null) throw new Error('should be available with old stats');
143
+ if(cache.get('/test.js', newStats) !== null) throw new Error('should be invalidated with newer stats');
144
+
145
+ cache.destroy();
146
+ log('✓ File modification detection working');
147
+ pass('File modification invalidation verified');
148
+ } catch(e){
149
+ cache?.destroy();
150
+ fail(e.message);
151
+ }
152
+ },
153
+
154
+ 'memory limit enforcement evicts oldest entries': async ({pass, fail, log}) => {
155
+ try {
156
+ const cache = createTestCache({
157
+ maxSize: 100,
158
+ maxMemoryMB: 0.002,
159
+ ttlMs: 60000,
160
+ watchFiles: false
161
+ });
162
+
163
+ const mockStats = { mtime: new Date('2023-01-01') };
164
+
165
+ cache.set('/test1.js', { default: () => 'test1' }, mockStats, 1);
166
+ cache.set('/test2.js', { default: () => 'test2' }, mockStats, 1);
167
+ cache.set('/test3.js', { default: () => 'test3' }, mockStats, 1);
168
+
169
+ if(cache.get('/test1.js', mockStats) !== null) throw new Error('first entry should be evicted due to memory limit');
170
+ if(cache.get('/test2.js', mockStats) === null) throw new Error('second entry should still be cached');
171
+ if(cache.get('/test3.js', mockStats) === null) throw new Error('third entry should still be cached');
172
+
173
+ cache.destroy();
174
+ log('✓ Memory limit enforcement working');
175
+ pass('Memory limit enforcement verified');
176
+ } catch(e){
177
+ cache?.destroy();
178
+ fail(e.message);
179
+ }
180
+ },
181
+
182
+ 'statistics tracking works correctly': async ({pass, fail, log}) => {
183
+ try {
184
+ const cache = createTestCache({
185
+ maxSize: 10,
186
+ watchFiles: false
187
+ });
188
+
189
+ const mockStats = { mtime: new Date('2023-01-01') };
190
+
191
+ if(cache.get('/test.js', mockStats) !== null) throw new Error('should be cache miss');
192
+ if(cache.stats.misses !== 1) throw new Error('miss count should be 1');
193
+
194
+ cache.set('/test.js', { default: () => 'test' }, mockStats, 1);
195
+ if(cache.get('/test.js', mockStats) === null) throw new Error('should be cache hit');
196
+ if(cache.stats.hits !== 1) throw new Error('hit count should be 1');
197
+
198
+ if(cache.getHitRate() !== 50) throw new Error('hit rate should be 50%');
199
+
200
+ const stats = cache.getStats();
201
+ if(!stats.cache || !stats.stats || !stats.memory) throw new Error('stats structure invalid');
202
+
203
+ cache.destroy();
204
+ log('✓ Statistics tracking accurate');
205
+ pass('Statistics tracking verified');
206
+ } catch(e){
207
+ cache?.destroy();
208
+ fail(e.message);
209
+ }
210
+ },
211
+
212
+ 'file watching invalidates cache on changes': async ({pass, fail, log}) => {
213
+ let cache;
214
+ let testFilePath;
215
+ try {
216
+ log('Starting file watching test...');
217
+ const tempDir = await createTempDir();
218
+ log('Created temp directory');
219
+
220
+ cache = createTestCache({
221
+ maxSize: 10,
222
+ ttlMs: 60000,
223
+ watchFiles: true
224
+ });
225
+ log('Created cache with file watching enabled');
226
+
227
+ testFilePath = path.join(tempDir, 'watch-test.js');
228
+
229
+ await writeFile(testFilePath, 'export default () => "v1";');
230
+ log('Wrote initial test file');
231
+ const initialStats = await stat(testFilePath);
232
+
233
+ const module = { default: () => 'v1' };
234
+ cache.set(testFilePath, module, initialStats, 1);
235
+ log('Set initial cache entry');
236
+
237
+ if(cache.get(testFilePath, initialStats) === null) throw new Error('should be in cache initially');
238
+ log('Verified initial cache entry exists');
239
+
240
+ await new Promise(resolve => setTimeout(resolve, 50));
241
+ await writeFile(testFilePath, 'export default () => "v2";');
242
+ log('Modified test file');
243
+
244
+ await new Promise(resolve => setTimeout(resolve, 200));
245
+ log('Waited for file watcher to detect changes');
246
+
247
+ if(cache.get(testFilePath, initialStats) !== null) throw new Error('should be invalidated after file change');
248
+ if(cache.stats.fileChanges === 0) throw new Error('file change should be tracked');
249
+ log('Verified cache invalidation worked');
250
+
251
+ // Clean up immediately
252
+ await unlink(testFilePath);
253
+ log('Deleted test file');
254
+ cache.destroy();
255
+ log('Destroyed cache');
256
+ cache = null; // Ensure it's not destroyed again in catch
257
+
258
+ // Give extra time for file watcher cleanup
259
+ await new Promise(resolve => setTimeout(resolve, 100));
260
+ log('File watching test cleanup complete');
261
+
262
+ log('✓ File watching invalidation working');
263
+ pass('File watching verified');
264
+ } catch(e){
265
+ log(`Error in file watching test: ${e.message}`);
266
+ if (testFilePath) {
267
+ try { await unlink(testFilePath); log('Cleaned up test file after error'); } catch {}
268
+ }
269
+ cache?.destroy();
270
+ log('Destroyed cache after error');
271
+ fail(e.message);
272
+ }
273
+ }
274
+ };
@@ -40,7 +40,7 @@ export default {
40
40
  process.chdir(dir);
41
41
  const flags = {root: '.', logging: 0, scan: true};
42
42
  const handler = await router(flags, log);
43
- const server = http.createServer(handler);
43
+ const server = http.createServer(handler);
44
44
  const port = randomPort();
45
45
  await new Promise(r => server.listen(port, r));
46
46
  await new Promise(r => setTimeout(r, 50));
@@ -67,7 +67,9 @@ export default {
67
67
  const flags = {root: '.', logging: 0, scan: false};
68
68
  const logFn = () => {};
69
69
  // write config before init
70
- await write(dir, '.config.json', JSON.stringify({customRoutes: {'/a': fileA, 'b/*': path.join(dir, 'b/*')}}));
70
+ await write(dir, '.config.json', JSON.stringify({
71
+ customRoutes: {'/a': fileA, 'b/*': path.join(dir, 'b/*')}
72
+ }));
71
73
  const handler = await router(flags, logFn);
72
74
  const server = http.createServer(handler);
73
75
  const port = randomPort();
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+
3
+ /*
4
+ Cache Demo Script
5
+ Demonstrates the module cache functionality with real-time monitoring
6
+ */
7
+
8
+ import path from 'path';
9
+ import { writeFile, unlink, mkdir, rm } from 'fs/promises';
10
+ import ModuleCache from '../src/moduleCache.js';
11
+
12
+ const createDemoFiles = async (demoDir) => {
13
+ await mkdir(demoDir, { recursive: true });
14
+
15
+ // Create some demo route files
16
+ await writeFile(path.join(demoDir, 'users.js'),
17
+ 'export default async (req, res) => res.json({ users: ["alice", "bob"] });'
18
+ );
19
+
20
+ await writeFile(path.join(demoDir, 'posts.js'),
21
+ 'export default async (req, res) => res.json({ posts: ["post1", "post2"] });'
22
+ );
23
+
24
+ await writeFile(path.join(demoDir, 'auth.js'),
25
+ 'export default async (req, res) => res.json({ authenticated: true });'
26
+ );
27
+
28
+ return [
29
+ path.join(demoDir, 'users.js'),
30
+ path.join(demoDir, 'posts.js'),
31
+ path.join(demoDir, 'auth.js')
32
+ ];
33
+ };
34
+
35
+ const simulateFileAccess = async (cache, filePaths) => {
36
+ const { stat } = await import('fs/promises');
37
+
38
+ for(const filePath of filePaths) {
39
+ const stats = await stat(filePath);
40
+
41
+ // Try to get from cache first
42
+ let module = cache.get(filePath, stats);
43
+
44
+ if(!module) {
45
+ // Simulate loading module
46
+ const fileUrl = `file://${filePath}?t=${Date.now()}`;
47
+ module = await import(fileUrl);
48
+ cache.set(filePath, module, stats, stats.size / 1024);
49
+ }
50
+
51
+ console.log(`📁 ${path.basename(filePath)}: ${module ? '✅ cached' : '❌ loaded fresh'}`);
52
+ }
53
+ };
54
+
55
+ const displayCacheStats = (cache) => {
56
+ console.log('\n📊 Cache Statistics:');
57
+ console.log('─'.repeat(40));
58
+
59
+ const stats = cache.getStats();
60
+ console.log(`Entries: ${stats.cache.size}/${stats.cache.maxSize}`);
61
+ console.log(`Memory: ${stats.cache.memoryUsageMB}/${stats.cache.maxMemoryMB}MB`);
62
+ console.log(`Hit Rate: ${cache.getHitRate()}%`);
63
+ console.log(`Hits: ${stats.stats.hits}, Misses: ${stats.stats.misses}`);
64
+ console.log(`File Changes: ${stats.stats.fileChanges}`);
65
+ console.log(`Heap Usage: ${stats.memory.heapUsedMB}MB (${stats.memory.heapUsagePercent}%)`);
66
+
67
+ if(stats.cache.size > 0) {
68
+ console.log('\n📋 Cached Files:');
69
+ const cachedFiles = cache.getCachedFiles();
70
+ cachedFiles.forEach(file => {
71
+ const ageSeconds = Math.round(file.age / 1000);
72
+ console.log(` • ${file.relativePath} (${file.sizeKB.toFixed(1)}KB, ${ageSeconds}s old)`);
73
+ });
74
+ }
75
+ console.log();
76
+ };
77
+
78
+ const main = async () => {
79
+ console.log('🚀 Kempo Server Module Cache Demo\n');
80
+
81
+ // Create demo directory and files
82
+ const demoDir = path.join(process.cwd(), 'demo-cache-test');
83
+ console.log('📝 Creating demo files...');
84
+ const filePaths = await createDemoFiles(demoDir);
85
+
86
+ // Initialize cache with small limits for demo
87
+ const cache = new ModuleCache({
88
+ maxSize: 2,
89
+ maxMemoryMB: 1,
90
+ ttlMs: 5000, // 5 seconds
91
+ watchFiles: true,
92
+ enableMemoryMonitoring: true
93
+ });
94
+
95
+ console.log('✅ Cache initialized\n');
96
+
97
+ try {
98
+ // Round 1: Initial loads (cache misses)
99
+ console.log('🔄 Round 1: Initial file access (expect cache misses)');
100
+ await simulateFileAccess(cache, filePaths);
101
+ displayCacheStats(cache);
102
+
103
+ // Round 2: Immediate re-access (cache hits)
104
+ console.log('🔄 Round 2: Immediate re-access (expect cache hits)');
105
+ await simulateFileAccess(cache, [filePaths[0], filePaths[1]]);
106
+ displayCacheStats(cache);
107
+
108
+ // Round 3: Add third file (should evict oldest due to maxSize=2)
109
+ console.log('🔄 Round 3: Access third file (expect LRU eviction)');
110
+ await simulateFileAccess(cache, [filePaths[2]]);
111
+ displayCacheStats(cache);
112
+
113
+ // Round 4: Wait for TTL expiration
114
+ console.log('⏱️ Waiting 6 seconds for TTL expiration...');
115
+ await new Promise(resolve => setTimeout(resolve, 6000));
116
+
117
+ console.log('🔄 Round 4: Access after TTL expiration (expect cache misses)');
118
+ await simulateFileAccess(cache, [filePaths[1]]);
119
+ displayCacheStats(cache);
120
+
121
+ // Round 5: Demonstrate file watching
122
+ console.log('📝 Round 5: Modifying file to test file watching...');
123
+ await writeFile(filePaths[1],
124
+ 'export default async (req, res) => res.json({ posts: ["updated post"] });'
125
+ );
126
+
127
+ // Wait for file watcher to detect change
128
+ await new Promise(resolve => setTimeout(resolve, 200));
129
+
130
+ console.log('🔄 Accessing modified file (expect cache invalidation)');
131
+ await simulateFileAccess(cache, [filePaths[1]]);
132
+ displayCacheStats(cache);
133
+
134
+ } finally {
135
+ // Cleanup
136
+ console.log('🧹 Cleaning up...');
137
+ cache.destroy();
138
+ await rm(demoDir, { recursive: true });
139
+ console.log('✅ Demo completed successfully!');
140
+ }
141
+ };
142
+
143
+ if(import.meta.url === `file://${process.argv[1]}`) {
144
+ main().catch(console.error);
145
+ }
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+
3
+ /*
4
+ Cache Monitor Utility
5
+ Displays real-time cache statistics and memory usage
6
+ */
7
+
8
+ import { spawn } from 'child_process';
9
+ import { pathToFileURL } from 'url';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const formatBytes = (bytes) => {
14
+ const mb = bytes / 1024 / 1024;
15
+ return `${mb.toFixed(2)}MB`;
16
+ };
17
+
18
+ const formatPercent = (value) => `${value.toFixed(1)}%`;
19
+
20
+ const clearScreen = () => process.stdout.write('\x1b[2J\x1b[0f');
21
+
22
+ const displayStats = (stats) => {
23
+ clearScreen();
24
+ console.log('='.repeat(60));
25
+ console.log('📦 KEMPO SERVER - MODULE CACHE MONITOR');
26
+ console.log('='.repeat(60));
27
+ console.log();
28
+
29
+ if (!stats) {
30
+ console.log('❌ Cache not available or disabled');
31
+ return;
32
+ }
33
+
34
+ const { cache, stats: cacheStats, memory, config } = stats;
35
+
36
+ // Cache Status
37
+ console.log('📊 CACHE STATUS');
38
+ console.log('-'.repeat(30));
39
+ console.log(`Entries: ${cache.size}/${cache.maxSize} (${((cache.size/cache.maxSize)*100).toFixed(1)}%)`);
40
+ console.log(`Memory: ${cache.memoryUsageMB}/${cache.maxMemoryMB}MB (${((cache.memoryUsageMB/cache.maxMemoryMB)*100).toFixed(1)}%)`);
41
+ console.log(`Watchers: ${cache.watchersActive} active`);
42
+ console.log();
43
+
44
+ // Performance Stats
45
+ console.log('⚡ PERFORMANCE');
46
+ console.log('-'.repeat(30));
47
+ const total = cacheStats.hits + cacheStats.misses;
48
+ const hitRate = total === 0 ? 0 : (cacheStats.hits / total * 100);
49
+ console.log(`Hit Rate: ${hitRate.toFixed(1)}% (${cacheStats.hits} hits, ${cacheStats.misses} misses)`);
50
+ console.log(`Evictions: ${cacheStats.evictions}`);
51
+ console.log(`File Changes: ${cacheStats.fileChanges}`);
52
+ console.log();
53
+
54
+ // Memory Info
55
+ console.log('🧠 NODE.JS MEMORY');
56
+ console.log('-'.repeat(30));
57
+ console.log(`Heap Used: ${memory.heapUsedMB}MB (${memory.heapUsagePercent}%)`);
58
+ console.log(`Heap Total: ${memory.heapTotalMB}MB`);
59
+ console.log(`RSS: ${memory.rssMB}MB`);
60
+ console.log();
61
+
62
+ // Configuration
63
+ console.log('⚙️ CONFIGURATION');
64
+ console.log('-'.repeat(30));
65
+ console.log(`TTL: ${Math.round(config.ttlMs / 1000)}s`);
66
+ console.log(`Max Heap: ${config.maxHeapUsagePercent}%`);
67
+ console.log(`File Watch: ${config.watchFiles ? '✅' : '❌'}`);
68
+ console.log();
69
+
70
+ console.log(`Last updated: ${new Date().toLocaleTimeString()}`);
71
+ console.log('Press Ctrl+C to exit');
72
+ };
73
+
74
+ // Simulate getting stats from a running server
75
+ // In a real implementation, you'd expose an endpoint or use IPC
76
+ const getStatsFromServer = async () => {
77
+ // This is a placeholder - in practice you'd either:
78
+ // 1. Add an admin endpoint to your server that returns cache stats
79
+ // 2. Use process communication to get stats from the running server
80
+ // 3. Log stats to a file that this script reads
81
+
82
+ // For demonstration, return mock stats
83
+ return {
84
+ cache: {
85
+ size: Math.floor(Math.random() * 100),
86
+ maxSize: 100,
87
+ memoryUsageMB: Math.random() * 50,
88
+ maxMemoryMB: 50,
89
+ watchersActive: Math.floor(Math.random() * 50)
90
+ },
91
+ stats: {
92
+ hits: Math.floor(Math.random() * 1000),
93
+ misses: Math.floor(Math.random() * 200),
94
+ evictions: Math.floor(Math.random() * 50),
95
+ fileChanges: Math.floor(Math.random() * 10)
96
+ },
97
+ memory: {
98
+ heapUsedMB: 45 + Math.random() * 20,
99
+ heapTotalMB: 70 + Math.random() * 30,
100
+ heapUsagePercent: 60 + Math.random() * 20,
101
+ rssMB: 80 + Math.random() * 40
102
+ },
103
+ config: {
104
+ ttlMs: 300000,
105
+ maxHeapUsagePercent: 70,
106
+ watchFiles: true
107
+ }
108
+ };
109
+ };
110
+
111
+ const main = async () => {
112
+ console.log('Starting cache monitor...');
113
+
114
+ setInterval(async () => {
115
+ try {
116
+ const stats = await getStatsFromServer();
117
+ displayStats(stats);
118
+ } catch (error) {
119
+ console.error('Error getting stats:', error.message);
120
+ }
121
+ }, 2000); // Update every 2 seconds
122
+ };
123
+
124
+ // Handle Ctrl+C gracefully
125
+ process.on('SIGINT', () => {
126
+ console.log('\n\n👋 Cache monitor stopped');
127
+ process.exit(0);
128
+ });
129
+
130
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
131
+ main();
132
+ }