bertui 1.1.8 โ 1.2.0
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/index.js +65 -45
- package/package.json +5 -2
- package/src/analyzer/index.js +370 -0
- package/src/build/compiler/route-discoverer.js +2 -0
- package/src/build/processors/css-builder.js +116 -80
- package/src/build.js +104 -93
- package/src/cli.js +83 -18
- package/src/client/compiler.js +168 -67
- package/src/css/processor.js +46 -1
- package/src/dev.js +47 -9
- package/src/hydration/index.js +151 -0
- package/src/layouts/index.js +165 -0
- package/src/loading/index.js +210 -0
- package/src/middleware/index.js +182 -0
- package/src/scaffolder/index.js +310 -0
- package/src/serve.js +195 -0
- package/src/server/dev-handler.js +78 -148
- package/src/server/dev-server-utils.js +16 -5
- package/src/server-islands/index.js +1 -1
- package/src/utils/cache.js +297 -0
|
@@ -1,45 +1,28 @@
|
|
|
1
|
-
// bertui/src/server/dev-handler.js -
|
|
2
|
-
// Headless dev server handler for Bunny integration
|
|
3
|
-
|
|
1
|
+
// bertui/src/server/dev-handler.js - WITH MIDDLEWARE + LAYOUTS + LOADING
|
|
4
2
|
import { join, extname, dirname } from 'path';
|
|
5
|
-
import { existsSync
|
|
3
|
+
import { existsSync } from 'fs';
|
|
6
4
|
import logger from '../logger/logger.js';
|
|
7
5
|
import { compileProject } from '../client/compiler.js';
|
|
8
6
|
import { loadConfig } from '../config/loadConfig.js';
|
|
9
7
|
import { getContentType, getImageContentType, serveHTML, setupFileWatcher } from './dev-server-utils.js';
|
|
10
8
|
|
|
11
|
-
/**
|
|
12
|
-
* Create a headless dev server handler for integration with Elysia/Bunny
|
|
13
|
-
* @param {Object} options
|
|
14
|
-
* @param {string} options.root - Project root directory
|
|
15
|
-
* @param {number} options.port - Port number (for HMR WebSocket URL)
|
|
16
|
-
* @param {Object} options.elysiaApp - Optional Elysia app instance to mount on
|
|
17
|
-
* @returns {Promise<Object>} Handler object with request handler and utilities
|
|
18
|
-
*/
|
|
19
9
|
export async function createDevHandler(options = {}) {
|
|
20
10
|
const root = options.root || process.cwd();
|
|
21
11
|
const port = parseInt(options.port) || 3000;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
const middlewareManager = options.middleware || null;
|
|
13
|
+
const layouts = options.layouts || {};
|
|
14
|
+
const loadingComponents = options.loadingComponents || {};
|
|
15
|
+
|
|
25
16
|
const compiledDir = join(root, '.bertui', 'compiled');
|
|
26
17
|
const stylesDir = join(root, '.bertui', 'styles');
|
|
27
18
|
const srcDir = join(root, 'src');
|
|
28
19
|
const publicDir = join(root, 'public');
|
|
29
|
-
|
|
20
|
+
|
|
30
21
|
const config = await loadConfig(root);
|
|
31
|
-
|
|
32
|
-
let hasRouter =
|
|
33
|
-
const routerPath = join(compiledDir, 'router.js');
|
|
34
|
-
if (existsSync(routerPath)) {
|
|
35
|
-
hasRouter = true;
|
|
36
|
-
logger.info('File-based routing enabled');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Clients set for HMR
|
|
22
|
+
|
|
23
|
+
let hasRouter = existsSync(join(compiledDir, 'router.js'));
|
|
40
24
|
const clients = new Set();
|
|
41
|
-
|
|
42
|
-
// WebSocket handler for HMR
|
|
25
|
+
|
|
43
26
|
const websocketHandler = {
|
|
44
27
|
open(ws) {
|
|
45
28
|
clients.add(ws);
|
|
@@ -47,208 +30,155 @@ export async function createDevHandler(options = {}) {
|
|
|
47
30
|
},
|
|
48
31
|
close(ws) {
|
|
49
32
|
clients.delete(ws);
|
|
50
|
-
|
|
51
|
-
}
|
|
33
|
+
},
|
|
52
34
|
};
|
|
53
|
-
|
|
54
|
-
// Notify all HMR clients
|
|
35
|
+
|
|
55
36
|
function notifyClients(message) {
|
|
56
37
|
for (const client of clients) {
|
|
57
|
-
try {
|
|
58
|
-
|
|
59
|
-
} catch (e) {
|
|
60
|
-
clients.delete(client);
|
|
61
|
-
}
|
|
38
|
+
try { client.send(JSON.stringify(message)); }
|
|
39
|
+
catch (e) { clients.delete(client); }
|
|
62
40
|
}
|
|
63
41
|
}
|
|
64
|
-
|
|
65
|
-
// Setup file watcher if we have a root
|
|
42
|
+
|
|
66
43
|
let watcherCleanup = null;
|
|
67
44
|
if (root) {
|
|
68
45
|
watcherCleanup = setupFileWatcher(root, compiledDir, clients, async () => {
|
|
69
46
|
hasRouter = existsSync(join(compiledDir, 'router.js'));
|
|
70
47
|
});
|
|
71
48
|
}
|
|
72
|
-
|
|
73
|
-
// MAIN REQUEST HANDLER
|
|
49
|
+
|
|
74
50
|
async function handleRequest(request) {
|
|
75
51
|
const url = new URL(request.url);
|
|
76
|
-
|
|
77
|
-
//
|
|
52
|
+
|
|
53
|
+
// WebSocket upgrade for HMR
|
|
78
54
|
if (url.pathname === '/__hmr' && request.headers.get('upgrade') === 'websocket') {
|
|
79
|
-
// This will be handled by Elysia/Bun.serve upgrade mechanism
|
|
80
55
|
return { type: 'websocket', handler: websocketHandler };
|
|
81
56
|
}
|
|
82
|
-
|
|
83
|
-
//
|
|
57
|
+
|
|
58
|
+
// โ
Run middleware BEFORE every page request
|
|
59
|
+
if (middlewareManager && isPageRequest(url.pathname)) {
|
|
60
|
+
const middlewareResponse = await middlewareManager.run(request, {
|
|
61
|
+
route: url.pathname,
|
|
62
|
+
});
|
|
63
|
+
if (middlewareResponse) {
|
|
64
|
+
logger.debug(`๐ก๏ธ Middleware handled: ${url.pathname}`);
|
|
65
|
+
return middlewareResponse;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Serve page HTML
|
|
84
70
|
if (url.pathname === '/' || (!url.pathname.includes('.') && !url.pathname.startsWith('/compiled'))) {
|
|
85
71
|
const html = await serveHTML(root, hasRouter, config, port);
|
|
86
72
|
return new Response(html, {
|
|
87
|
-
headers: { 'Content-Type': 'text/html' }
|
|
73
|
+
headers: { 'Content-Type': 'text/html' },
|
|
88
74
|
});
|
|
89
75
|
}
|
|
90
|
-
|
|
91
|
-
//
|
|
76
|
+
|
|
77
|
+
// Compiled JS (includes layouts and loading components)
|
|
92
78
|
if (url.pathname.startsWith('/compiled/')) {
|
|
93
79
|
const filepath = join(compiledDir, url.pathname.replace('/compiled/', ''));
|
|
94
80
|
const file = Bun.file(filepath);
|
|
95
|
-
|
|
96
|
-
if (await file.exists()) {
|
|
97
|
-
const ext = extname(filepath).toLowerCase();
|
|
98
|
-
const contentType = ext === '.js' ? 'application/javascript; charset=utf-8' : 'text/plain';
|
|
99
|
-
|
|
100
|
-
return new Response(file, {
|
|
101
|
-
headers: {
|
|
102
|
-
'Content-Type': contentType,
|
|
103
|
-
'Cache-Control': 'no-store, no-cache, must-revalidate'
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Serve bertui-animate CSS
|
|
110
|
-
if (url.pathname === '/bertui-animate.css') {
|
|
111
|
-
const bertuiAnimatePath = join(root, 'node_modules/bertui-animate/dist/bertui-animate.min.css');
|
|
112
|
-
const file = Bun.file(bertuiAnimatePath);
|
|
113
|
-
|
|
114
81
|
if (await file.exists()) {
|
|
115
82
|
return new Response(file, {
|
|
116
|
-
headers: {
|
|
117
|
-
'Content-Type': '
|
|
118
|
-
'Cache-Control': 'no-store'
|
|
119
|
-
}
|
|
83
|
+
headers: {
|
|
84
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
85
|
+
'Cache-Control': 'no-store',
|
|
86
|
+
},
|
|
120
87
|
});
|
|
121
88
|
}
|
|
122
89
|
}
|
|
123
|
-
|
|
124
|
-
//
|
|
90
|
+
|
|
91
|
+
// CSS
|
|
125
92
|
if (url.pathname.startsWith('/styles/')) {
|
|
126
93
|
const filepath = join(stylesDir, url.pathname.replace('/styles/', ''));
|
|
127
94
|
const file = Bun.file(filepath);
|
|
128
|
-
|
|
129
95
|
if (await file.exists()) {
|
|
130
96
|
return new Response(file, {
|
|
131
|
-
headers: {
|
|
132
|
-
'Content-Type': 'text/css',
|
|
133
|
-
'Cache-Control': 'no-store'
|
|
134
|
-
}
|
|
97
|
+
headers: { 'Content-Type': 'text/css', 'Cache-Control': 'no-store' },
|
|
135
98
|
});
|
|
136
99
|
}
|
|
137
100
|
}
|
|
138
|
-
|
|
139
|
-
//
|
|
101
|
+
|
|
102
|
+
// bertui-animate CSS
|
|
103
|
+
if (url.pathname === '/bertui-animate.css') {
|
|
104
|
+
const animPath = join(root, 'node_modules/bertui-animate/dist/bertui-animate.min.css');
|
|
105
|
+
const file = Bun.file(animPath);
|
|
106
|
+
if (await file.exists()) {
|
|
107
|
+
return new Response(file, { headers: { 'Content-Type': 'text/css' } });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Images
|
|
140
112
|
if (url.pathname.startsWith('/images/')) {
|
|
141
113
|
const filepath = join(srcDir, 'images', url.pathname.replace('/images/', ''));
|
|
142
114
|
const file = Bun.file(filepath);
|
|
143
|
-
|
|
144
115
|
if (await file.exists()) {
|
|
145
116
|
const ext = extname(filepath).toLowerCase();
|
|
146
|
-
const contentType = getImageContentType(ext);
|
|
147
|
-
|
|
148
117
|
return new Response(file, {
|
|
149
|
-
headers: {
|
|
150
|
-
'Content-Type': contentType,
|
|
151
|
-
'Cache-Control': 'no-cache'
|
|
152
|
-
}
|
|
118
|
+
headers: { 'Content-Type': getImageContentType(ext), 'Cache-Control': 'no-cache' },
|
|
153
119
|
});
|
|
154
120
|
}
|
|
155
121
|
}
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
if (url.pathname.startsWith('/public/')) {
|
|
122
|
+
|
|
123
|
+
// Public directory
|
|
124
|
+
if (url.pathname.startsWith('/public/') || existsSync(join(publicDir, url.pathname.slice(1)))) {
|
|
159
125
|
const filepath = join(publicDir, url.pathname.replace('/public/', ''));
|
|
160
126
|
const file = Bun.file(filepath);
|
|
161
|
-
|
|
162
127
|
if (await file.exists()) {
|
|
163
|
-
return new Response(file, {
|
|
164
|
-
headers: { 'Cache-Control': 'no-cache' }
|
|
165
|
-
});
|
|
128
|
+
return new Response(file, { headers: { 'Cache-Control': 'no-cache' } });
|
|
166
129
|
}
|
|
167
130
|
}
|
|
168
|
-
|
|
169
|
-
//
|
|
131
|
+
|
|
132
|
+
// node_modules
|
|
170
133
|
if (url.pathname.startsWith('/node_modules/')) {
|
|
171
134
|
const filepath = join(root, 'node_modules', url.pathname.replace('/node_modules/', ''));
|
|
172
135
|
const file = Bun.file(filepath);
|
|
173
|
-
|
|
174
136
|
if (await file.exists()) {
|
|
175
137
|
const ext = extname(filepath).toLowerCase();
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
contentType = 'text/css';
|
|
180
|
-
} else if (ext === '.js' || ext === '.mjs') {
|
|
181
|
-
contentType = 'application/javascript; charset=utf-8';
|
|
182
|
-
} else {
|
|
183
|
-
contentType = getContentType(ext);
|
|
184
|
-
}
|
|
185
|
-
|
|
138
|
+
const contentType = ext === '.css' ? 'text/css' :
|
|
139
|
+
['.js', '.mjs'].includes(ext) ? 'application/javascript; charset=utf-8' :
|
|
140
|
+
getContentType(ext);
|
|
186
141
|
return new Response(file, {
|
|
187
|
-
headers: {
|
|
188
|
-
'Content-Type': contentType,
|
|
189
|
-
'Cache-Control': 'no-cache'
|
|
190
|
-
}
|
|
142
|
+
headers: { 'Content-Type': contentType, 'Cache-Control': 'no-cache' },
|
|
191
143
|
});
|
|
192
144
|
}
|
|
193
145
|
}
|
|
194
|
-
|
|
195
|
-
// Not a BertUI route
|
|
146
|
+
|
|
196
147
|
return null;
|
|
197
148
|
}
|
|
198
|
-
|
|
199
|
-
// Standalone server starter (for backward compatibility)
|
|
149
|
+
|
|
200
150
|
async function start() {
|
|
201
151
|
const server = Bun.serve({
|
|
202
152
|
port,
|
|
203
153
|
async fetch(req, server) {
|
|
204
154
|
const url = new URL(req.url);
|
|
205
|
-
|
|
206
|
-
// Handle WebSocket upgrade
|
|
207
155
|
if (url.pathname === '/__hmr') {
|
|
208
156
|
const success = server.upgrade(req);
|
|
209
157
|
if (success) return undefined;
|
|
210
158
|
return new Response('WebSocket upgrade failed', { status: 500 });
|
|
211
159
|
}
|
|
212
|
-
|
|
213
|
-
// Handle normal requests
|
|
214
160
|
const response = await handleRequest(req);
|
|
215
161
|
if (response) return response;
|
|
216
|
-
|
|
217
162
|
return new Response('Not found', { status: 404 });
|
|
218
163
|
},
|
|
219
|
-
websocket: websocketHandler
|
|
164
|
+
websocket: websocketHandler,
|
|
220
165
|
});
|
|
221
|
-
|
|
222
|
-
logger.success(`๐ BertUI
|
|
166
|
+
|
|
167
|
+
logger.success(`๐ BertUI running at http://localhost:${port}`);
|
|
223
168
|
return server;
|
|
224
169
|
}
|
|
225
|
-
|
|
226
|
-
// Recompile project
|
|
227
|
-
async function recompile() {
|
|
228
|
-
return await compileProject(root);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Cleanup
|
|
170
|
+
|
|
232
171
|
function dispose() {
|
|
233
|
-
if (watcherCleanup
|
|
234
|
-
watcherCleanup();
|
|
235
|
-
}
|
|
172
|
+
if (watcherCleanup) watcherCleanup();
|
|
236
173
|
clients.clear();
|
|
174
|
+
if (middlewareManager) middlewareManager.dispose();
|
|
237
175
|
}
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
handleRequest,
|
|
241
|
-
start,
|
|
242
|
-
recompile,
|
|
243
|
-
dispose,
|
|
244
|
-
notifyClients,
|
|
245
|
-
config,
|
|
246
|
-
hasRouter,
|
|
247
|
-
// For Elysia integration
|
|
248
|
-
getElysiaApp: () => elysiaApp,
|
|
249
|
-
websocketHandler
|
|
250
|
-
};
|
|
176
|
+
|
|
177
|
+
return { handleRequest, start, notifyClients, dispose, config, hasRouter, websocketHandler };
|
|
251
178
|
}
|
|
252
179
|
|
|
253
|
-
|
|
254
|
-
|
|
180
|
+
function isPageRequest(pathname) {
|
|
181
|
+
// Skip asset requests
|
|
182
|
+
return !pathname.includes('.') ||
|
|
183
|
+
pathname.endsWith('.html');
|
|
184
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
// bertui/src/server/dev-server-utils.js -
|
|
2
|
-
// Shared utilities for dev server (extracted from dev-server.js)
|
|
3
|
-
|
|
1
|
+
// bertui/src/server/dev-server-utils.js - WITH CACHE IMPORT
|
|
4
2
|
import { join, extname } from 'path';
|
|
5
3
|
import { existsSync, readdirSync, watch } from 'fs';
|
|
6
4
|
import logger from '../logger/logger.js';
|
|
7
5
|
import { compileProject } from '../client/compiler.js';
|
|
6
|
+
import { globalCache } from '../utils/cache.js'; // โ
Now this works!
|
|
8
7
|
|
|
9
8
|
// Image content type mapping
|
|
10
9
|
export function getImageContentType(ext) {
|
|
@@ -48,8 +47,17 @@ export function getContentType(ext) {
|
|
|
48
47
|
return types[ext] || 'text/plain';
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
// HTML generator
|
|
50
|
+
// HTML generator with caching
|
|
52
51
|
export async function serveHTML(root, hasRouter, config, port) {
|
|
52
|
+
const cacheKey = `html:${root}:${port}`;
|
|
53
|
+
|
|
54
|
+
// Try cache first
|
|
55
|
+
const cached = globalCache.get(cacheKey, { ttl: 1000 }); // 1 second cache during dev
|
|
56
|
+
if (cached) {
|
|
57
|
+
logger.debug('โก Serving cached HTML');
|
|
58
|
+
return cached;
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
const meta = config.meta || {};
|
|
54
62
|
|
|
55
63
|
const srcStylesDir = join(root, 'src', 'styles');
|
|
@@ -203,10 +211,13 @@ ${bertuiAnimateStylesheet}
|
|
|
203
211
|
</body>
|
|
204
212
|
</html>`;
|
|
205
213
|
|
|
214
|
+
// Cache the HTML
|
|
215
|
+
globalCache.set(cacheKey, html, { ttl: 1000 });
|
|
216
|
+
|
|
206
217
|
return html;
|
|
207
218
|
}
|
|
208
219
|
|
|
209
|
-
// File watcher setup
|
|
220
|
+
// File watcher setup (unchanged)
|
|
210
221
|
export function setupFileWatcher(root, compiledDir, clients, onRecompile) {
|
|
211
222
|
const srcDir = join(root, 'src');
|
|
212
223
|
const configPath = join(root, 'bertui.config.js');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { validateServerIsland } from '../build/server-island-validator.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function extractStaticHTML(componentCode) {
|
|
7
7
|
// For now, use existing regex-based extractor
|
|
8
8
|
// TODO: Replace with proper AST parser
|
|
9
9
|
return extractJSXFromReturn(componentCode);
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// bertui/src/utils/cache.js - ULTRA FAST CACHING (Microsecond precision)
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import logger from '../logger/logger.js';
|
|
4
|
+
|
|
5
|
+
export class BertuiCache {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.maxSize = options.maxSize || 5000;
|
|
8
|
+
this.ttl = options.ttl || 30000; // 30 seconds default
|
|
9
|
+
this.stats = { hits: 0, misses: 0, sets: 0, evictions: 0 };
|
|
10
|
+
|
|
11
|
+
// Main cache store
|
|
12
|
+
this.store = new Map();
|
|
13
|
+
|
|
14
|
+
// File content cache with timestamps
|
|
15
|
+
this.fileCache = new Map();
|
|
16
|
+
this.fileTimestamps = new Map();
|
|
17
|
+
|
|
18
|
+
// Compiled code cache (keyed by content hash)
|
|
19
|
+
this.codeCache = new Map();
|
|
20
|
+
|
|
21
|
+
// CSS processing cache
|
|
22
|
+
this.cssCache = new Map();
|
|
23
|
+
|
|
24
|
+
// Image optimization cache
|
|
25
|
+
this.imageCache = new Map();
|
|
26
|
+
|
|
27
|
+
// Weak reference cache for DOM objects (if in browser)
|
|
28
|
+
if (typeof WeakRef !== 'undefined') {
|
|
29
|
+
this.weakCache = new Map();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Start periodic cleanup
|
|
33
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ULTRA FAST GET with microsecond timing
|
|
37
|
+
get(key, options = {}) {
|
|
38
|
+
const start = process.hrtime.bigint();
|
|
39
|
+
|
|
40
|
+
const item = this.store.get(key);
|
|
41
|
+
if (!item) {
|
|
42
|
+
this.stats.misses++;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check TTL
|
|
47
|
+
const ttl = options.ttl || item.ttl || this.ttl;
|
|
48
|
+
if (Date.now() - item.timestamp > ttl) {
|
|
49
|
+
this.store.delete(key);
|
|
50
|
+
this.stats.misses++;
|
|
51
|
+
this.stats.evictions++;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.stats.hits++;
|
|
56
|
+
|
|
57
|
+
// Update access time for LRU
|
|
58
|
+
item.lastAccessed = Date.now();
|
|
59
|
+
|
|
60
|
+
if (options.logSpeed) {
|
|
61
|
+
const end = process.hrtime.bigint();
|
|
62
|
+
const duration = Number(end - start) / 1000; // Microseconds
|
|
63
|
+
logger.debug(`โก Cache hit: ${duration.toFixed(3)}ยตs for ${key.substring(0, 30)}...`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return item.value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
set(key, value, options = {}) {
|
|
70
|
+
// Generate hash for large values to save memory
|
|
71
|
+
const valueHash = typeof value === 'string' && value.length > 10000
|
|
72
|
+
? createHash('md5').update(value).digest('hex')
|
|
73
|
+
: null;
|
|
74
|
+
|
|
75
|
+
this.store.set(key, {
|
|
76
|
+
value: valueHash ? { __hash: valueHash, __original: null } : value,
|
|
77
|
+
valueHash,
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
lastAccessed: Date.now(),
|
|
80
|
+
ttl: options.ttl || this.ttl,
|
|
81
|
+
size: this.getSize(value)
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.stats.sets++;
|
|
85
|
+
|
|
86
|
+
// Store original separately if hashed
|
|
87
|
+
if (valueHash) {
|
|
88
|
+
this.codeCache.set(valueHash, value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// LRU cleanup if needed
|
|
92
|
+
if (this.store.size > this.maxSize) {
|
|
93
|
+
this.evictLRU();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// FILE CACHE: Zero-copy file reading with mtime validation
|
|
98
|
+
async getFile(filePath, options = {}) {
|
|
99
|
+
const cacheKey = `file:${filePath}`;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const file = Bun.file(filePath);
|
|
103
|
+
const exists = await file.exists();
|
|
104
|
+
if (!exists) return null;
|
|
105
|
+
|
|
106
|
+
const stats = await file.stat();
|
|
107
|
+
const mtimeMs = stats.mtimeMs;
|
|
108
|
+
|
|
109
|
+
// Check cache
|
|
110
|
+
const cached = this.fileCache.get(cacheKey);
|
|
111
|
+
const cachedTime = this.fileTimestamps.get(cacheKey);
|
|
112
|
+
|
|
113
|
+
if (cached && cachedTime === mtimeMs) {
|
|
114
|
+
if (options.logSpeed) {
|
|
115
|
+
logger.debug(`๐ File cache hit: ${filePath.split('/').pop()}`);
|
|
116
|
+
}
|
|
117
|
+
return cached;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Read file (single operation)
|
|
121
|
+
const start = process.hrtime.bigint();
|
|
122
|
+
const content = await file.arrayBuffer();
|
|
123
|
+
const buffer = Buffer.from(content);
|
|
124
|
+
const end = process.hrtime.bigint();
|
|
125
|
+
|
|
126
|
+
// Store in cache
|
|
127
|
+
this.fileCache.set(cacheKey, buffer);
|
|
128
|
+
this.fileTimestamps.set(cacheKey, mtimeMs);
|
|
129
|
+
|
|
130
|
+
if (options.logSpeed) {
|
|
131
|
+
const duration = Number(end - start) / 1000;
|
|
132
|
+
logger.debug(`๐ File read: ${duration.toFixed(3)}ยตs - ${filePath.split('/').pop()}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return buffer;
|
|
136
|
+
|
|
137
|
+
} catch (error) {
|
|
138
|
+
logger.error(`File cache error: ${filePath} - ${error.message}`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// CODE TRANSFORMATION CACHE
|
|
144
|
+
getTransformed(sourceCode, options = {}) {
|
|
145
|
+
const hash = createHash('md5')
|
|
146
|
+
.update(sourceCode)
|
|
147
|
+
.update(JSON.stringify(options))
|
|
148
|
+
.digest('hex');
|
|
149
|
+
|
|
150
|
+
const cacheKey = `transform:${hash}`;
|
|
151
|
+
return this.get(cacheKey, options);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
setTransformed(sourceCode, result, options = {}) {
|
|
155
|
+
const hash = createHash('md5')
|
|
156
|
+
.update(sourceCode)
|
|
157
|
+
.update(JSON.stringify(options))
|
|
158
|
+
.digest('hex');
|
|
159
|
+
|
|
160
|
+
const cacheKey = `transform:${hash}`;
|
|
161
|
+
this.set(cacheKey, result, options);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// CSS PROCESSING CACHE
|
|
165
|
+
getCSS(css, options = {}) {
|
|
166
|
+
const hash = createHash('md5')
|
|
167
|
+
.update(css)
|
|
168
|
+
.update(JSON.stringify(options))
|
|
169
|
+
.digest('hex');
|
|
170
|
+
|
|
171
|
+
return this.cssCache.get(hash);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
setCSS(css, result, options = {}) {
|
|
175
|
+
const hash = createHash('md5')
|
|
176
|
+
.update(css)
|
|
177
|
+
.update(JSON.stringify(options))
|
|
178
|
+
.digest('hex');
|
|
179
|
+
|
|
180
|
+
this.cssCache.set(hash, result);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// BATCH OPERATIONS
|
|
184
|
+
mget(keys) {
|
|
185
|
+
const results = [];
|
|
186
|
+
for (const key of keys) {
|
|
187
|
+
results.push(this.get(key));
|
|
188
|
+
}
|
|
189
|
+
return results;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
mset(entries) {
|
|
193
|
+
for (const [key, value] of entries) {
|
|
194
|
+
this.set(key, value);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// STATS with microsecond precision
|
|
199
|
+
getStats() {
|
|
200
|
+
const total = this.stats.hits + this.stats.misses;
|
|
201
|
+
const hitRate = total > 0 ? (this.stats.hits / total * 100).toFixed(2) : 0;
|
|
202
|
+
|
|
203
|
+
// Memory usage
|
|
204
|
+
const memUsage = process.memoryUsage();
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
hits: this.stats.hits,
|
|
208
|
+
misses: this.stats.misses,
|
|
209
|
+
sets: this.stats.sets,
|
|
210
|
+
evictions: this.stats.evictions,
|
|
211
|
+
hitRate: `${hitRate}%`,
|
|
212
|
+
size: this.store.size,
|
|
213
|
+
fileCacheSize: this.fileCache.size,
|
|
214
|
+
codeCacheSize: this.codeCache.size,
|
|
215
|
+
cssCacheSize: this.cssCache.size,
|
|
216
|
+
imageCacheSize: this.imageCache.size,
|
|
217
|
+
memory: {
|
|
218
|
+
heapUsed: `${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`,
|
|
219
|
+
heapTotal: `${(memUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
|
|
220
|
+
rss: `${(memUsage.rss / 1024 / 1024).toFixed(2)} MB`
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// PRIVATE METHODS
|
|
226
|
+
getSize(value) {
|
|
227
|
+
if (typeof value === 'string') return value.length;
|
|
228
|
+
if (Buffer.isBuffer(value)) return value.length;
|
|
229
|
+
if (typeof value === 'object') return JSON.stringify(value).length;
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
evictLRU() {
|
|
234
|
+
const entries = Array.from(this.store.entries());
|
|
235
|
+
entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed);
|
|
236
|
+
|
|
237
|
+
const removeCount = Math.floor(this.maxSize * 0.2); // Remove 20%
|
|
238
|
+
for (let i = 0; i < removeCount && i < entries.length; i++) {
|
|
239
|
+
this.store.delete(entries[i][0]);
|
|
240
|
+
this.stats.evictions++;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
cleanup() {
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
let evicted = 0;
|
|
247
|
+
|
|
248
|
+
for (const [key, item] of this.store.entries()) {
|
|
249
|
+
if (now - item.timestamp > item.ttl) {
|
|
250
|
+
this.store.delete(key);
|
|
251
|
+
evicted++;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (evicted > 0) {
|
|
256
|
+
logger.debug(`๐งน Cache cleanup: removed ${evicted} expired items`);
|
|
257
|
+
this.stats.evictions += evicted;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
dispose() {
|
|
262
|
+
if (this.cleanupInterval) {
|
|
263
|
+
clearInterval(this.cleanupInterval);
|
|
264
|
+
}
|
|
265
|
+
this.store.clear();
|
|
266
|
+
this.fileCache.clear();
|
|
267
|
+
this.fileTimestamps.clear();
|
|
268
|
+
this.codeCache.clear();
|
|
269
|
+
this.cssCache.clear();
|
|
270
|
+
this.imageCache.clear();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Singleton instance
|
|
275
|
+
export const globalCache = new BertuiCache();
|
|
276
|
+
|
|
277
|
+
// Decorator for automatic caching of async functions
|
|
278
|
+
export function cached(options = {}) {
|
|
279
|
+
return function(target, propertyKey, descriptor) {
|
|
280
|
+
const originalMethod = descriptor.value;
|
|
281
|
+
|
|
282
|
+
descriptor.value = async function(...args) {
|
|
283
|
+
const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
|
|
284
|
+
const cached = globalCache.get(cacheKey, options);
|
|
285
|
+
|
|
286
|
+
if (cached !== null) {
|
|
287
|
+
return cached;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const result = await originalMethod.apply(this, args);
|
|
291
|
+
globalCache.set(cacheKey, result, options);
|
|
292
|
+
return result;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
return descriptor;
|
|
296
|
+
};
|
|
297
|
+
}
|