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
@@ -5,84 +5,104 @@ import router from '../src/router.js';
5
5
  import defaultConfig from '../src/defaultConfig.js';
6
6
 
7
7
  export default {
8
- 'serves static files and 404s unknown': async ({pass, fail, log}) => {
9
- try {
10
- await withTempDir(async (dir) => {
11
- await write(dir, 'index.html', '<h1>Home</h1>');
12
- const prev = process.cwd();
13
- process.chdir(dir);
14
- const flags = {root: '.', logging: 0, scan: false};
15
- const logFn = () => {};
16
- const handler = await router(flags, logFn);
17
- const server = http.createServer(handler);
18
- const port = randomPort();
19
- await new Promise(r => server.listen(port, r));
20
- await new Promise(r => setTimeout(r, 50));
21
- try {
22
- const ok = await httpGet(`http://localhost:${port}/index.html`);
23
- log('ok status: ' + ok.res.statusCode);
24
- if(ok.res.statusCode !== 200) throw new Error('static 200');
25
- const miss = await httpGet(`http://localhost:${port}/nope`);
26
- log('miss status: ' + miss.res.statusCode);
27
- if(miss.res.statusCode !== 404) throw new Error('404');
28
- } finally {
29
- server.close();
8
+ 'serves static files and 404s unknown': async ({pass, fail}) => {
9
+ await withTempDir(async (dir) => {
10
+ await write(dir, 'index.html', '<h1>Home</h1>');
11
+ const prev = process.cwd();
12
+ process.chdir(dir);
13
+ const flags = {root: '.', logging: 0, scan: false};
14
+ const logFn = () => {};
15
+ const handler = await router(flags, logFn);
16
+ const server = http.createServer(handler);
17
+ const port = randomPort();
18
+ await new Promise(r => server.listen(port, r));
19
+ await new Promise(r => setTimeout(r, 50));
20
+
21
+ const ok = await httpGet(`http://localhost:${port}/index.html`);
22
+ if(ok.res.statusCode !== 200) {
23
+ server.close();
24
+ process.chdir(prev);
25
+ return fail('static 200');
26
+ }
27
+
28
+ const miss = await httpGet(`http://localhost:${port}/nope`);
29
+ if(miss.res.statusCode !== 404) {
30
+ server.close();
31
+ process.chdir(prev);
32
+ return fail('404');
33
+ }
34
+
35
+ server.close();
30
36
  process.chdir(prev);
31
- }
32
- });
33
- pass('static + 404');
34
- } catch(e){ fail(e.message); }
37
+ });
38
+ pass('static + 404');
35
39
  },
36
- 'rescan on 404 when enabled and not blacklisted': async ({pass, fail, log}) => {
37
- try {
38
- await withTempDir(async (dir) => {
39
- const prev = process.cwd();
40
- process.chdir(dir);
41
- const flags = {root: '.', logging: 0, scan: true};
42
- const handler = await router(flags, log);
43
- const server = http.createServer(handler);
44
- const port = randomPort();
45
- await new Promise(r => server.listen(port, r));
46
- await new Promise(r => setTimeout(r, 50));
47
- try {
48
- const miss1 = await httpGet(`http://localhost:${port}/late.html`);
49
- log('first miss: ' + miss1.res.statusCode);
50
- if(miss1.res.statusCode !== 404) throw new Error('first 404');
51
- await write(dir, 'late.html', 'later');
52
- const hit = await httpGet(`http://localhost:${port}/late.html`);
53
- log('hit after rescan: ' + hit.res.statusCode);
54
- if(hit.res.statusCode !== 200) throw new Error('served after rescan');
55
- } finally { server.close(); process.chdir(prev); }
56
- });
57
- pass('rescan');
58
- } catch(e){ fail(e.message); }
40
+ 'rescan on 404 when enabled and not blacklisted': async ({pass, fail}) => {
41
+ await withTempDir(async (dir) => {
42
+ const prev = process.cwd();
43
+ process.chdir(dir);
44
+ const flags = {root: '.', logging: 0, scan: true};
45
+ const handler = await router(flags, log);
46
+ const server = http.createServer(handler);
47
+ const port = randomPort();
48
+ await new Promise(r => server.listen(port, r));
49
+ await new Promise(r => setTimeout(r, 50));
50
+
51
+ const miss1 = await httpGet(`http://localhost:${port}/late.html`);
52
+ if(miss1.res.statusCode !== 404) {
53
+ server.close();
54
+ process.chdir(prev);
55
+ return fail('first 404');
56
+ }
57
+
58
+ await write(dir, 'late.html', 'later');
59
+ const hit = await httpGet(`http://localhost:${port}/late.html`);
60
+ if(hit.res.statusCode !== 200) {
61
+ server.close();
62
+ process.chdir(prev);
63
+ return fail('served after rescan');
64
+ }
65
+
66
+ server.close();
67
+ process.chdir(prev);
68
+ });
69
+ pass('rescan');
59
70
  },
60
- 'custom and wildcard routes serve mapped files': async ({pass, fail, log}) => {
61
- try {
62
- await withTempDir(async (dir) => {
63
- const fileA = await write(dir, 'a.txt', 'A');
64
- const fileB = await write(dir, 'b/1.txt', 'B1');
65
- const prev = process.cwd();
66
- process.chdir(dir);
67
- const flags = {root: '.', logging: 0, scan: false};
68
- const logFn = () => {};
69
- // write config before init
70
- await write(dir, '.config.json', JSON.stringify({customRoutes: {'/a': fileA, 'b/*': path.join(dir, 'b/*')}}));
71
- const handler = await router(flags, logFn);
72
- const server = http.createServer(handler);
73
- const port = randomPort();
74
- await new Promise(r => server.listen(port, r));
75
- await new Promise(r => setTimeout(r, 50));
76
- try {
77
- const r1 = await httpGet(`http://localhost:${port}/a`);
78
- log('custom status: ' + r1.res.statusCode);
79
- if(r1.body.toString() !== 'A') throw new Error('custom route');
80
- const r2 = await httpGet(`http://localhost:${port}/b/1.txt`);
81
- log('wildcard status: ' + r2.res.statusCode);
82
- if(r2.body.toString() !== 'B1') throw new Error('wildcard');
83
- } finally { server.close(); process.chdir(prev); }
84
- });
85
- pass('custom+wildcard');
86
- } catch(e){ fail(e.message); }
71
+ 'custom and wildcard routes serve mapped files': async ({pass, fail}) => {
72
+ await withTempDir(async (dir) => {
73
+ const fileA = await write(dir, 'a.txt', 'A');
74
+ const fileB = await write(dir, 'b/1.txt', 'B1');
75
+ const prev = process.cwd();
76
+ process.chdir(dir);
77
+ const flags = {root: '.', logging: 0, scan: false};
78
+ const logFn = () => {};
79
+ // write config before init
80
+ await write(dir, '.config.json', JSON.stringify({
81
+ customRoutes: {'/a': fileA, 'b/*': path.join(dir, 'b/*')}
82
+ }));
83
+ const handler = await router(flags, logFn);
84
+ const server = http.createServer(handler);
85
+ const port = randomPort();
86
+ await new Promise(r => server.listen(port, r));
87
+ await new Promise(r => setTimeout(r, 50));
88
+
89
+ const r1 = await httpGet(`http://localhost:${port}/a`);
90
+ if(r1.body.toString() !== 'A') {
91
+ server.close();
92
+ process.chdir(prev);
93
+ return fail('custom route');
94
+ }
95
+
96
+ const r2 = await httpGet(`http://localhost:${port}/b/1.txt`);
97
+ if(r2.body.toString() !== 'B1') {
98
+ server.close();
99
+ process.chdir(prev);
100
+ return fail('wildcard');
101
+ }
102
+
103
+ server.close();
104
+ process.chdir(prev);
105
+ });
106
+ pass('custom+wildcard');
87
107
  }
88
108
  };
@@ -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
+ }