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.
@@ -1,45 +1,28 @@
1
- // bertui/src/server/dev-handler.js - NEW FILE
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, readdirSync } from 'fs';
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 elysiaApp = options.elysiaApp || null;
23
-
24
- // Initialize BertUI state
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 = false;
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
- logger.debug(`HMR client disconnected (${clients.size} remaining)`);
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
- client.send(JSON.stringify(message));
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
- // Handle WebSocket upgrade for HMR
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
- // Serve HTML for routes
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
- // Serve compiled JavaScript
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': 'text/css',
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
- // Serve CSS
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
- // Serve images from src/images/
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
- // Serve from public/
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
- // Serve node_modules
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
- let contentType;
177
-
178
- if (ext === '.css') {
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 standalone server running at http://localhost:${port}`);
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 && typeof watcherCleanup === 'function') {
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
- // Re-export for convenience
254
- export { handleRequest as standaloneHandler } from './request-handler.js';
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 - NEW FILE
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
- export function extractStaticHTML(componentCode) {
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
+ }