tywrap 0.1.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/LICENSE +21 -0
- package/README.md +319 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +263 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +41 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +234 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/analyzer.d.ts +63 -0
- package/dist/core/analyzer.d.ts.map +1 -0
- package/dist/core/analyzer.js +676 -0
- package/dist/core/analyzer.js.map +1 -0
- package/dist/core/discovery.d.ts +85 -0
- package/dist/core/discovery.d.ts.map +1 -0
- package/dist/core/discovery.js +344 -0
- package/dist/core/discovery.js.map +1 -0
- package/dist/core/generator.d.ts +37 -0
- package/dist/core/generator.d.ts.map +1 -0
- package/dist/core/generator.js +368 -0
- package/dist/core/generator.js.map +1 -0
- package/dist/core/mapper.d.ts +40 -0
- package/dist/core/mapper.d.ts.map +1 -0
- package/dist/core/mapper.js +401 -0
- package/dist/core/mapper.js.map +1 -0
- package/dist/core/validation.d.ts +79 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +486 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/base.d.ts +12 -0
- package/dist/runtime/base.d.ts.map +1 -0
- package/dist/runtime/base.js +6 -0
- package/dist/runtime/base.js.map +1 -0
- package/dist/runtime/errors.d.ts +17 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +21 -0
- package/dist/runtime/errors.js.map +1 -0
- package/dist/runtime/http.d.ts +22 -0
- package/dist/runtime/http.d.ts.map +1 -0
- package/dist/runtime/http.js +79 -0
- package/dist/runtime/http.js.map +1 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/node.d.ts +48 -0
- package/dist/runtime/node.d.ts.map +1 -0
- package/dist/runtime/node.js +328 -0
- package/dist/runtime/node.js.map +1 -0
- package/dist/runtime/optimized-node.d.ts +131 -0
- package/dist/runtime/optimized-node.d.ts.map +1 -0
- package/dist/runtime/optimized-node.js +622 -0
- package/dist/runtime/optimized-node.js.map +1 -0
- package/dist/runtime/protocol.d.ts +3 -0
- package/dist/runtime/protocol.d.ts.map +1 -0
- package/dist/runtime/protocol.js +3 -0
- package/dist/runtime/protocol.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +27 -0
- package/dist/runtime/pyodide.d.ts.map +1 -0
- package/dist/runtime/pyodide.js +191 -0
- package/dist/runtime/pyodide.js.map +1 -0
- package/dist/tools/python_suite.js +173 -0
- package/dist/tools/python_suite.js.map +1 -0
- package/dist/types/index.d.ts +299 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/tywrap.d.ts +29 -0
- package/dist/tywrap.d.ts.map +1 -0
- package/dist/tywrap.js +589 -0
- package/dist/tywrap.js.map +1 -0
- package/dist/utils/bundle-optimizer.d.ts +182 -0
- package/dist/utils/bundle-optimizer.d.ts.map +1 -0
- package/dist/utils/bundle-optimizer.js +680 -0
- package/dist/utils/bundle-optimizer.js.map +1 -0
- package/dist/utils/cache.d.ts +149 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +492 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/codec.d.ts +94 -0
- package/dist/utils/codec.d.ts.map +1 -0
- package/dist/utils/codec.js +150 -0
- package/dist/utils/codec.js.map +1 -0
- package/dist/utils/logger.d.ts +42 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +140 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/memory-profiler.d.ts +123 -0
- package/dist/utils/memory-profiler.d.ts.map +1 -0
- package/dist/utils/memory-profiler.js +426 -0
- package/dist/utils/memory-profiler.js.map +1 -0
- package/dist/utils/parallel-processor.d.ts +146 -0
- package/dist/utils/parallel-processor.d.ts.map +1 -0
- package/dist/utils/parallel-processor.js +636 -0
- package/dist/utils/parallel-processor.js.map +1 -0
- package/dist/utils/python.d.ts +8 -0
- package/dist/utils/python.d.ts.map +1 -0
- package/dist/utils/python.js +56 -0
- package/dist/utils/python.js.map +1 -0
- package/dist/utils/runtime.d.ts +153 -0
- package/dist/utils/runtime.d.ts.map +1 -0
- package/dist/utils/runtime.js +519 -0
- package/dist/utils/runtime.js.map +1 -0
- package/package.json +136 -0
- package/runtime/python_bridge.py +476 -0
- package/src/cli.ts +331 -0
- package/src/config/index.ts +289 -0
- package/src/core/analyzer.ts +788 -0
- package/src/core/discovery.ts +415 -0
- package/src/core/generator.ts +447 -0
- package/src/core/mapper.ts +504 -0
- package/src/core/validation.ts +693 -0
- package/src/index.ts +83 -0
- package/src/runtime/base.ts +32 -0
- package/src/runtime/errors.ts +19 -0
- package/src/runtime/http.ts +132 -0
- package/src/runtime/index.ts +24 -0
- package/src/runtime/node.ts +431 -0
- package/src/runtime/optimized-node.ts +838 -0
- package/src/runtime/protocol.ts +2 -0
- package/src/runtime/pyodide.ts +228 -0
- package/src/types/global.d.ts +60 -0
- package/src/types/index.ts +410 -0
- package/src/tywrap.ts +669 -0
- package/src/utils/bundle-optimizer.ts +876 -0
- package/src/utils/cache.ts +634 -0
- package/src/utils/codec.ts +275 -0
- package/src/utils/logger.ts +201 -0
- package/src/utils/memory-profiler.ts +582 -0
- package/src/utils/parallel-processor.ts +879 -0
- package/src/utils/python.ts +79 -0
- package/src/utils/runtime.ts +616 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intelligent Caching System for tywrap
|
|
3
|
+
* Provides multi-level caching with invalidation strategies and performance monitoring
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
9
|
+
|
|
10
|
+
import type { AnalysisResult, PythonModule, GeneratedCode } from '../types/index.js';
|
|
11
|
+
import { getComponentLogger } from './logger.js';
|
|
12
|
+
|
|
13
|
+
const log = getComponentLogger('Cache');
|
|
14
|
+
|
|
15
|
+
export interface CacheEntry<T = unknown> {
|
|
16
|
+
key: string;
|
|
17
|
+
data: T;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
version: string;
|
|
20
|
+
dependencies: string[];
|
|
21
|
+
metadata: {
|
|
22
|
+
size: number;
|
|
23
|
+
hitCount: number;
|
|
24
|
+
lastAccessed: number;
|
|
25
|
+
computeTime: number; // Time to generate this entry
|
|
26
|
+
memoryFootprint: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CacheStats {
|
|
31
|
+
totalEntries: number;
|
|
32
|
+
totalSize: number;
|
|
33
|
+
hitRate: number;
|
|
34
|
+
averageComputeTime: number;
|
|
35
|
+
topKeys: Array<{ key: string; hitCount: number; size: number }>;
|
|
36
|
+
memoryUsage: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CacheConfig {
|
|
40
|
+
baseDir: string;
|
|
41
|
+
maxSize: number; // Maximum cache size in bytes
|
|
42
|
+
maxAge: number; // Maximum age in milliseconds
|
|
43
|
+
maxEntries: number; // Maximum number of entries
|
|
44
|
+
compressionEnabled: boolean;
|
|
45
|
+
persistToDisk: boolean;
|
|
46
|
+
cleanupInterval: number; // Cleanup interval in milliseconds
|
|
47
|
+
debug?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class IntelligentCache {
|
|
51
|
+
private memoryCache = new Map<string, CacheEntry>();
|
|
52
|
+
private stats = {
|
|
53
|
+
hits: 0,
|
|
54
|
+
misses: 0,
|
|
55
|
+
evictions: 0,
|
|
56
|
+
totalComputeTime: 0,
|
|
57
|
+
};
|
|
58
|
+
private config: CacheConfig;
|
|
59
|
+
private cleanupTimer?: NodeJS.Timeout;
|
|
60
|
+
private compressionAvailable = false;
|
|
61
|
+
private debug = false;
|
|
62
|
+
|
|
63
|
+
constructor(config: Partial<CacheConfig> = {}) {
|
|
64
|
+
this.config = {
|
|
65
|
+
baseDir: join(process.cwd(), '.tywrap-cache'),
|
|
66
|
+
maxSize: 100 * 1024 * 1024, // 100MB
|
|
67
|
+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
68
|
+
maxEntries: 1000,
|
|
69
|
+
compressionEnabled: true,
|
|
70
|
+
persistToDisk: true,
|
|
71
|
+
cleanupInterval: 60 * 60 * 1000, // 1 hour
|
|
72
|
+
debug: false,
|
|
73
|
+
...config,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
this.debug = this.config.debug ?? false;
|
|
77
|
+
|
|
78
|
+
if (this.config.persistToDisk) {
|
|
79
|
+
this.safeMkdir(this.config.baseDir).catch(error => {
|
|
80
|
+
log.warn('Failed to create cache directory', { error: String(error) });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check for compression availability
|
|
85
|
+
try {
|
|
86
|
+
require.resolve('zlib');
|
|
87
|
+
this.compressionAvailable = this.config.compressionEnabled;
|
|
88
|
+
} catch {
|
|
89
|
+
this.compressionAvailable = false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Start periodic cleanup
|
|
93
|
+
if (this.config.cleanupInterval > 0) {
|
|
94
|
+
this.startCleanupScheduler();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (this.config.persistToDisk) {
|
|
98
|
+
this.safeMkdir(this.config.baseDir)
|
|
99
|
+
.then(() => this.loadFromDisk())
|
|
100
|
+
.catch(error => {
|
|
101
|
+
log.warn('Failed to initialize disk cache', { error: String(error) });
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setDebug(debug: boolean): void {
|
|
107
|
+
this.debug = debug;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private debugLog(message: string): void {
|
|
111
|
+
if (!this.debug) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
process.stdout.write(`${message}\n`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async safeMkdir(path: string): Promise<void> {
|
|
118
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- cache path is internal
|
|
119
|
+
await fs.mkdir(path, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async safeReaddir(path: string): Promise<string[]> {
|
|
123
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- cache path is internal
|
|
124
|
+
return fs.readdir(path);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async safeReadFile(path: string): Promise<string> {
|
|
128
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- cache path is internal
|
|
129
|
+
return fs.readFile(path, 'utf8');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async safeWriteFile(path: string, data: string): Promise<void> {
|
|
133
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- cache path is internal
|
|
134
|
+
await fs.writeFile(path, data);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async safeUnlink(path: string): Promise<void> {
|
|
138
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- cache path is internal
|
|
139
|
+
await fs.unlink(path);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate cache key from input parameters
|
|
144
|
+
*/
|
|
145
|
+
generateKey(prefix: string, ...inputs: unknown[]): string {
|
|
146
|
+
const hash = createHash('sha256');
|
|
147
|
+
|
|
148
|
+
// Add prefix
|
|
149
|
+
hash.update(prefix);
|
|
150
|
+
|
|
151
|
+
// Add inputs
|
|
152
|
+
for (const input of inputs) {
|
|
153
|
+
if (typeof input === 'string') {
|
|
154
|
+
hash.update(input);
|
|
155
|
+
} else if (Buffer.isBuffer(input)) {
|
|
156
|
+
hash.update(input);
|
|
157
|
+
} else if (input !== undefined && input !== null) {
|
|
158
|
+
hash.update(JSON.stringify(input));
|
|
159
|
+
} else {
|
|
160
|
+
hash.update('null');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return hash.digest('hex').substring(0, 16); // Use first 16 chars for readability
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get entry from cache with performance tracking
|
|
169
|
+
*/
|
|
170
|
+
async get<T>(key: string): Promise<T | null> {
|
|
171
|
+
const startTime = performance.now();
|
|
172
|
+
|
|
173
|
+
// Try memory cache first
|
|
174
|
+
const entry = this.memoryCache.get(key);
|
|
175
|
+
if (entry) {
|
|
176
|
+
// Check if entry is still valid
|
|
177
|
+
if (this.isEntryValid(entry)) {
|
|
178
|
+
entry.metadata.hitCount++;
|
|
179
|
+
entry.metadata.lastAccessed = Date.now();
|
|
180
|
+
this.stats.hits++;
|
|
181
|
+
|
|
182
|
+
this.debugLog(
|
|
183
|
+
`Cache HIT [${key}] (memory) - ${(performance.now() - startTime).toFixed(2)}ms`
|
|
184
|
+
);
|
|
185
|
+
return entry.data as T;
|
|
186
|
+
} else {
|
|
187
|
+
this.memoryCache.delete(key);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Try disk cache if enabled
|
|
192
|
+
if (this.config.persistToDisk) {
|
|
193
|
+
const diskEntry = await this.loadFromDiskKey<T>(key);
|
|
194
|
+
if (diskEntry && this.isEntryValid(diskEntry)) {
|
|
195
|
+
// Move to memory cache
|
|
196
|
+
this.memoryCache.set(key, diskEntry);
|
|
197
|
+
diskEntry.metadata.hitCount++;
|
|
198
|
+
diskEntry.metadata.lastAccessed = Date.now();
|
|
199
|
+
this.stats.hits++;
|
|
200
|
+
|
|
201
|
+
this.debugLog(
|
|
202
|
+
`Cache HIT [${key}] (disk) - ${(performance.now() - startTime).toFixed(2)}ms`
|
|
203
|
+
);
|
|
204
|
+
return diskEntry.data;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.stats.misses++;
|
|
209
|
+
this.debugLog(`Cache MISS [${key}] - ${(performance.now() - startTime).toFixed(2)}ms`);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Store entry in cache with metadata
|
|
215
|
+
*/
|
|
216
|
+
async set<T>(
|
|
217
|
+
key: string,
|
|
218
|
+
data: T,
|
|
219
|
+
options: {
|
|
220
|
+
dependencies?: string[];
|
|
221
|
+
computeTime?: number;
|
|
222
|
+
version?: string;
|
|
223
|
+
} = {}
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
const startTime = performance.now();
|
|
226
|
+
|
|
227
|
+
const entry: CacheEntry<T> = {
|
|
228
|
+
key,
|
|
229
|
+
data,
|
|
230
|
+
timestamp: Date.now(),
|
|
231
|
+
version: options.version ?? '1.0.0',
|
|
232
|
+
dependencies: options.dependencies ?? [],
|
|
233
|
+
metadata: {
|
|
234
|
+
size: this.estimateSize(data),
|
|
235
|
+
hitCount: 0,
|
|
236
|
+
lastAccessed: Date.now(),
|
|
237
|
+
computeTime: options.computeTime ?? 0,
|
|
238
|
+
memoryFootprint: process.memoryUsage().heapUsed,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Update stats
|
|
243
|
+
if (options.computeTime) {
|
|
244
|
+
this.stats.totalComputeTime += options.computeTime;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Store in memory cache
|
|
248
|
+
this.memoryCache.set(key, entry);
|
|
249
|
+
|
|
250
|
+
// Store to disk if enabled
|
|
251
|
+
if (this.config.persistToDisk) {
|
|
252
|
+
await this.saveToDiskKey(key, entry);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Trigger cleanup if needed
|
|
256
|
+
await this.cleanup();
|
|
257
|
+
|
|
258
|
+
this.debugLog(`Cache SET [${key}] - ${(performance.now() - startTime).toFixed(2)}ms`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Cached IR extraction with dependency tracking
|
|
263
|
+
*/
|
|
264
|
+
async getCachedAnalysis(
|
|
265
|
+
sourceCode: string,
|
|
266
|
+
modulePath: string,
|
|
267
|
+
version: string = '1.0.0'
|
|
268
|
+
): Promise<AnalysisResult | null> {
|
|
269
|
+
const dependencies = this.extractDependencies(sourceCode);
|
|
270
|
+
const key = this.generateKey('analysis', sourceCode, modulePath, version, dependencies);
|
|
271
|
+
|
|
272
|
+
const cached = await this.get<AnalysisResult>(key);
|
|
273
|
+
if (cached) {
|
|
274
|
+
return cached;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Store analysis result with dependency tracking
|
|
282
|
+
*/
|
|
283
|
+
async setCachedAnalysis(
|
|
284
|
+
sourceCode: string,
|
|
285
|
+
modulePath: string,
|
|
286
|
+
result: AnalysisResult,
|
|
287
|
+
computeTime: number,
|
|
288
|
+
version: string = '1.0.0'
|
|
289
|
+
): Promise<void> {
|
|
290
|
+
const dependencies = this.extractDependencies(sourceCode);
|
|
291
|
+
const key = this.generateKey('analysis', sourceCode, modulePath, version, dependencies);
|
|
292
|
+
|
|
293
|
+
await this.set(key, result, {
|
|
294
|
+
dependencies: [modulePath, ...dependencies],
|
|
295
|
+
computeTime,
|
|
296
|
+
version,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Cached code generation
|
|
302
|
+
*/
|
|
303
|
+
async getCachedGeneration(
|
|
304
|
+
module: PythonModule,
|
|
305
|
+
options: { moduleName: string; exportAll?: boolean },
|
|
306
|
+
version: string = '1.0.0'
|
|
307
|
+
): Promise<GeneratedCode | null> {
|
|
308
|
+
const key = this.generateKey('generation', module, options, version);
|
|
309
|
+
return this.get<GeneratedCode>(key);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Store generated code
|
|
314
|
+
*/
|
|
315
|
+
async setCachedGeneration(
|
|
316
|
+
module: PythonModule,
|
|
317
|
+
options: { moduleName: string; exportAll?: boolean },
|
|
318
|
+
result: GeneratedCode,
|
|
319
|
+
computeTime: number,
|
|
320
|
+
version: string = '1.0.0'
|
|
321
|
+
): Promise<void> {
|
|
322
|
+
const key = this.generateKey('generation', module, options, version);
|
|
323
|
+
|
|
324
|
+
await this.set(key, result, {
|
|
325
|
+
dependencies: [options.moduleName],
|
|
326
|
+
computeTime,
|
|
327
|
+
version,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Invalidate cache entries based on dependencies
|
|
333
|
+
*/
|
|
334
|
+
async invalidateByDependency(dependency: string): Promise<number> {
|
|
335
|
+
let invalidatedCount = 0;
|
|
336
|
+
|
|
337
|
+
// Invalidate memory cache
|
|
338
|
+
for (const [key, entry] of this.memoryCache) {
|
|
339
|
+
if (entry.dependencies.includes(dependency)) {
|
|
340
|
+
this.memoryCache.delete(key);
|
|
341
|
+
invalidatedCount++;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Invalidate disk cache if enabled
|
|
346
|
+
if (this.config.persistToDisk) {
|
|
347
|
+
const diskInvalidated = await this.invalidateDiskByDependency(dependency);
|
|
348
|
+
invalidatedCount += diskInvalidated;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
this.debugLog(`Invalidated ${invalidatedCount} cache entries for dependency: ${dependency}`);
|
|
352
|
+
return invalidatedCount;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get cache statistics
|
|
357
|
+
*/
|
|
358
|
+
getStats(): CacheStats {
|
|
359
|
+
const entries = Array.from(this.memoryCache.values());
|
|
360
|
+
const totalSize = entries.reduce((sum, entry) => sum + entry.metadata.size, 0);
|
|
361
|
+
const hitRate = this.stats.hits / (this.stats.hits + this.stats.misses);
|
|
362
|
+
const averageComputeTime = this.stats.totalComputeTime / Math.max(1, this.stats.hits);
|
|
363
|
+
|
|
364
|
+
const topKeys = entries
|
|
365
|
+
.sort((a, b) => b.metadata.hitCount - a.metadata.hitCount)
|
|
366
|
+
.slice(0, 10)
|
|
367
|
+
.map(entry => ({
|
|
368
|
+
key: entry.key,
|
|
369
|
+
hitCount: entry.metadata.hitCount,
|
|
370
|
+
size: entry.metadata.size,
|
|
371
|
+
}));
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
totalEntries: this.memoryCache.size,
|
|
375
|
+
totalSize,
|
|
376
|
+
hitRate: isNaN(hitRate) ? 0 : hitRate,
|
|
377
|
+
averageComputeTime,
|
|
378
|
+
topKeys,
|
|
379
|
+
memoryUsage: process.memoryUsage().heapUsed,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Clear all cache entries
|
|
385
|
+
*/
|
|
386
|
+
async clear(): Promise<void> {
|
|
387
|
+
this.memoryCache.clear();
|
|
388
|
+
if (this.config.persistToDisk) {
|
|
389
|
+
try {
|
|
390
|
+
const files = await this.safeReaddir(this.config.baseDir);
|
|
391
|
+
await Promise.all(
|
|
392
|
+
files
|
|
393
|
+
.filter(file => file.endsWith('.cache'))
|
|
394
|
+
.map(file =>
|
|
395
|
+
this.safeUnlink(join(this.config.baseDir, file)).catch(error =>
|
|
396
|
+
log.warn('Failed to remove cache file', { file, error: String(error) })
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
402
|
+
log.warn('Failed to clear disk cache', { error: String(error) });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
this.stats = { hits: 0, misses: 0, evictions: 0, totalComputeTime: 0 };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Cleanup old and large entries
|
|
412
|
+
*/
|
|
413
|
+
private async cleanup(): Promise<void> {
|
|
414
|
+
const entries = Array.from(this.memoryCache.entries());
|
|
415
|
+
let totalSize = entries.reduce((sum, [, entry]) => sum + entry.metadata.size, 0);
|
|
416
|
+
|
|
417
|
+
// Remove expired entries
|
|
418
|
+
const now = Date.now();
|
|
419
|
+
for (const [key, entry] of entries) {
|
|
420
|
+
if (now - entry.timestamp > this.config.maxAge) {
|
|
421
|
+
this.memoryCache.delete(key);
|
|
422
|
+
this.stats.evictions++;
|
|
423
|
+
totalSize -= entry.metadata.size;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Remove entries if over limit
|
|
428
|
+
if (this.memoryCache.size > this.config.maxEntries || totalSize > this.config.maxSize) {
|
|
429
|
+
// Sort by least recently used
|
|
430
|
+
const sorted = Array.from(this.memoryCache.entries()).sort(
|
|
431
|
+
(a, b) => a[1].metadata.lastAccessed - b[1].metadata.lastAccessed
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
while (
|
|
435
|
+
(this.memoryCache.size > this.config.maxEntries || totalSize > this.config.maxSize) &&
|
|
436
|
+
sorted.length > 0
|
|
437
|
+
) {
|
|
438
|
+
const next = sorted.shift();
|
|
439
|
+
if (!next) {
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
const [key, entry] = next;
|
|
443
|
+
this.memoryCache.delete(key);
|
|
444
|
+
this.stats.evictions++;
|
|
445
|
+
totalSize -= entry.metadata.size;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Check if cache entry is still valid
|
|
452
|
+
*/
|
|
453
|
+
private isEntryValid(entry: CacheEntry): boolean {
|
|
454
|
+
const now = Date.now();
|
|
455
|
+
return now - entry.timestamp < this.config.maxAge;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Estimate data size for cache management
|
|
460
|
+
*/
|
|
461
|
+
private estimateSize(data: unknown): number {
|
|
462
|
+
try {
|
|
463
|
+
return Buffer.byteLength(JSON.stringify(data), 'utf8');
|
|
464
|
+
} catch {
|
|
465
|
+
return 1024; // Default estimate
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Extract dependencies from Python source code
|
|
471
|
+
*/
|
|
472
|
+
private extractDependencies(sourceCode: string): string[] {
|
|
473
|
+
const dependencies: string[] = [];
|
|
474
|
+
const importRegex = /(?:from\s+(\w+)|import\s+(\w+))/g;
|
|
475
|
+
|
|
476
|
+
let match;
|
|
477
|
+
while ((match = importRegex.exec(sourceCode)) !== null) {
|
|
478
|
+
const module = match[1] ?? match[2];
|
|
479
|
+
if (module && !dependencies.includes(module)) {
|
|
480
|
+
dependencies.push(module);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return dependencies;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Load cache entries from disk
|
|
489
|
+
*/
|
|
490
|
+
private async loadFromDisk(): Promise<void> {
|
|
491
|
+
try {
|
|
492
|
+
const files = await this.safeReaddir(this.config.baseDir);
|
|
493
|
+
let loadedCount = 0;
|
|
494
|
+
|
|
495
|
+
for (const file of files) {
|
|
496
|
+
if (file.endsWith('.cache')) {
|
|
497
|
+
try {
|
|
498
|
+
const key = file.replace('.cache', '');
|
|
499
|
+
const entry = await this.loadFromDiskKey<unknown>(key);
|
|
500
|
+
|
|
501
|
+
if (entry && this.isEntryValid(entry)) {
|
|
502
|
+
this.memoryCache.set(key, entry);
|
|
503
|
+
loadedCount++;
|
|
504
|
+
}
|
|
505
|
+
} catch (error) {
|
|
506
|
+
log.warn('Failed to load cache file', { file, error: String(error) });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (loadedCount > 0) {
|
|
512
|
+
this.debugLog(`Loaded ${loadedCount} cache entries from disk`);
|
|
513
|
+
}
|
|
514
|
+
} catch (error) {
|
|
515
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
516
|
+
log.warn('Failed to load cache from disk', { error: String(error) });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Load specific key from disk
|
|
523
|
+
*/
|
|
524
|
+
private async loadFromDiskKey<T>(key: string): Promise<CacheEntry<T> | null> {
|
|
525
|
+
const filePath = join(this.config.baseDir, `${key}.cache`);
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
const data = await this.safeReadFile(filePath);
|
|
529
|
+
let parsed;
|
|
530
|
+
|
|
531
|
+
if (this.compressionAvailable && data.startsWith('COMPRESSED:')) {
|
|
532
|
+
const zlib = await import('zlib');
|
|
533
|
+
const compressed = Buffer.from(data.substring(11), 'base64');
|
|
534
|
+
const decompressed = zlib.gunzipSync(compressed);
|
|
535
|
+
parsed = JSON.parse(decompressed.toString('utf8'));
|
|
536
|
+
} else {
|
|
537
|
+
parsed = JSON.parse(data);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return parsed as CacheEntry<T>;
|
|
541
|
+
} catch (error) {
|
|
542
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
543
|
+
log.warn('Failed to load cache key', { key, error: String(error) });
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Save specific key to disk
|
|
551
|
+
*/
|
|
552
|
+
private async saveToDiskKey<T>(key: string, entry: CacheEntry<T>): Promise<void> {
|
|
553
|
+
const filePath = join(this.config.baseDir, `${key}.cache`);
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
// Ensure directory exists
|
|
557
|
+
await this.safeMkdir(dirname(filePath));
|
|
558
|
+
|
|
559
|
+
let data = JSON.stringify(entry);
|
|
560
|
+
|
|
561
|
+
// Compress if enabled and beneficial
|
|
562
|
+
if (this.compressionAvailable && data.length > 1024) {
|
|
563
|
+
try {
|
|
564
|
+
const zlib = await import('zlib');
|
|
565
|
+
const compressed = zlib.gzipSync(Buffer.from(data, 'utf8'));
|
|
566
|
+
if (compressed.length < data.length * 0.8) {
|
|
567
|
+
data = `COMPRESSED:${compressed.toString('base64')}`;
|
|
568
|
+
}
|
|
569
|
+
} catch (error) {
|
|
570
|
+
log.warn('Compression failed', { key, error: String(error) });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
await this.safeWriteFile(filePath, data);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
log.warn('Failed to save cache key', { key, error: String(error) });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Invalidate disk cache by dependency
|
|
582
|
+
*/
|
|
583
|
+
private async invalidateDiskByDependency(dependency: string): Promise<number> {
|
|
584
|
+
let invalidatedCount = 0;
|
|
585
|
+
try {
|
|
586
|
+
const files = await this.safeReaddir(this.config.baseDir);
|
|
587
|
+
|
|
588
|
+
for (const file of files) {
|
|
589
|
+
if (file.endsWith('.cache')) {
|
|
590
|
+
try {
|
|
591
|
+
const key = file.replace('.cache', '');
|
|
592
|
+
const entry = await this.loadFromDiskKey(key);
|
|
593
|
+
|
|
594
|
+
if (entry?.dependencies.includes(dependency)) {
|
|
595
|
+
await this.safeUnlink(join(this.config.baseDir, file));
|
|
596
|
+
invalidatedCount++;
|
|
597
|
+
}
|
|
598
|
+
} catch (error) {
|
|
599
|
+
log.warn('Failed to check cache file', { file, error: String(error) });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
} catch (error) {
|
|
604
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
605
|
+
log.warn('Failed to invalidate disk cache', { error: String(error) });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return invalidatedCount;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Start periodic cleanup scheduler
|
|
614
|
+
*/
|
|
615
|
+
private startCleanupScheduler(): void {
|
|
616
|
+
this.cleanupTimer = setInterval(async () => {
|
|
617
|
+
await this.cleanup();
|
|
618
|
+
}, this.config.cleanupInterval);
|
|
619
|
+
this.cleanupTimer.unref?.();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Dispose cache and cleanup resources
|
|
624
|
+
*/
|
|
625
|
+
dispose(): void {
|
|
626
|
+
if (this.cleanupTimer) {
|
|
627
|
+
clearInterval(this.cleanupTimer);
|
|
628
|
+
this.cleanupTimer = undefined;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Export singleton instance
|
|
634
|
+
export const globalCache = new IntelligentCache();
|