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.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +319 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +263 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/config/index.d.ts +41 -0
  8. package/dist/config/index.d.ts.map +1 -0
  9. package/dist/config/index.js +234 -0
  10. package/dist/config/index.js.map +1 -0
  11. package/dist/core/analyzer.d.ts +63 -0
  12. package/dist/core/analyzer.d.ts.map +1 -0
  13. package/dist/core/analyzer.js +676 -0
  14. package/dist/core/analyzer.js.map +1 -0
  15. package/dist/core/discovery.d.ts +85 -0
  16. package/dist/core/discovery.d.ts.map +1 -0
  17. package/dist/core/discovery.js +344 -0
  18. package/dist/core/discovery.js.map +1 -0
  19. package/dist/core/generator.d.ts +37 -0
  20. package/dist/core/generator.d.ts.map +1 -0
  21. package/dist/core/generator.js +368 -0
  22. package/dist/core/generator.js.map +1 -0
  23. package/dist/core/mapper.d.ts +40 -0
  24. package/dist/core/mapper.d.ts.map +1 -0
  25. package/dist/core/mapper.js +401 -0
  26. package/dist/core/mapper.js.map +1 -0
  27. package/dist/core/validation.d.ts +79 -0
  28. package/dist/core/validation.d.ts.map +1 -0
  29. package/dist/core/validation.js +486 -0
  30. package/dist/core/validation.js.map +1 -0
  31. package/dist/index.d.ts +26 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +29 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/runtime/base.d.ts +12 -0
  36. package/dist/runtime/base.d.ts.map +1 -0
  37. package/dist/runtime/base.js +6 -0
  38. package/dist/runtime/base.js.map +1 -0
  39. package/dist/runtime/errors.d.ts +17 -0
  40. package/dist/runtime/errors.d.ts.map +1 -0
  41. package/dist/runtime/errors.js +21 -0
  42. package/dist/runtime/errors.js.map +1 -0
  43. package/dist/runtime/http.d.ts +22 -0
  44. package/dist/runtime/http.d.ts.map +1 -0
  45. package/dist/runtime/http.js +79 -0
  46. package/dist/runtime/http.js.map +1 -0
  47. package/dist/runtime/index.d.ts +8 -0
  48. package/dist/runtime/index.d.ts.map +1 -0
  49. package/dist/runtime/index.js +17 -0
  50. package/dist/runtime/index.js.map +1 -0
  51. package/dist/runtime/node.d.ts +48 -0
  52. package/dist/runtime/node.d.ts.map +1 -0
  53. package/dist/runtime/node.js +328 -0
  54. package/dist/runtime/node.js.map +1 -0
  55. package/dist/runtime/optimized-node.d.ts +131 -0
  56. package/dist/runtime/optimized-node.d.ts.map +1 -0
  57. package/dist/runtime/optimized-node.js +622 -0
  58. package/dist/runtime/optimized-node.js.map +1 -0
  59. package/dist/runtime/protocol.d.ts +3 -0
  60. package/dist/runtime/protocol.d.ts.map +1 -0
  61. package/dist/runtime/protocol.js +3 -0
  62. package/dist/runtime/protocol.js.map +1 -0
  63. package/dist/runtime/pyodide.d.ts +27 -0
  64. package/dist/runtime/pyodide.d.ts.map +1 -0
  65. package/dist/runtime/pyodide.js +191 -0
  66. package/dist/runtime/pyodide.js.map +1 -0
  67. package/dist/tools/python_suite.js +173 -0
  68. package/dist/tools/python_suite.js.map +1 -0
  69. package/dist/types/index.d.ts +299 -0
  70. package/dist/types/index.d.ts.map +1 -0
  71. package/dist/types/index.js +5 -0
  72. package/dist/types/index.js.map +1 -0
  73. package/dist/tywrap.d.ts +29 -0
  74. package/dist/tywrap.d.ts.map +1 -0
  75. package/dist/tywrap.js +589 -0
  76. package/dist/tywrap.js.map +1 -0
  77. package/dist/utils/bundle-optimizer.d.ts +182 -0
  78. package/dist/utils/bundle-optimizer.d.ts.map +1 -0
  79. package/dist/utils/bundle-optimizer.js +680 -0
  80. package/dist/utils/bundle-optimizer.js.map +1 -0
  81. package/dist/utils/cache.d.ts +149 -0
  82. package/dist/utils/cache.d.ts.map +1 -0
  83. package/dist/utils/cache.js +492 -0
  84. package/dist/utils/cache.js.map +1 -0
  85. package/dist/utils/codec.d.ts +94 -0
  86. package/dist/utils/codec.d.ts.map +1 -0
  87. package/dist/utils/codec.js +150 -0
  88. package/dist/utils/codec.js.map +1 -0
  89. package/dist/utils/logger.d.ts +42 -0
  90. package/dist/utils/logger.d.ts.map +1 -0
  91. package/dist/utils/logger.js +140 -0
  92. package/dist/utils/logger.js.map +1 -0
  93. package/dist/utils/memory-profiler.d.ts +123 -0
  94. package/dist/utils/memory-profiler.d.ts.map +1 -0
  95. package/dist/utils/memory-profiler.js +426 -0
  96. package/dist/utils/memory-profiler.js.map +1 -0
  97. package/dist/utils/parallel-processor.d.ts +146 -0
  98. package/dist/utils/parallel-processor.d.ts.map +1 -0
  99. package/dist/utils/parallel-processor.js +636 -0
  100. package/dist/utils/parallel-processor.js.map +1 -0
  101. package/dist/utils/python.d.ts +8 -0
  102. package/dist/utils/python.d.ts.map +1 -0
  103. package/dist/utils/python.js +56 -0
  104. package/dist/utils/python.js.map +1 -0
  105. package/dist/utils/runtime.d.ts +153 -0
  106. package/dist/utils/runtime.d.ts.map +1 -0
  107. package/dist/utils/runtime.js +519 -0
  108. package/dist/utils/runtime.js.map +1 -0
  109. package/package.json +136 -0
  110. package/runtime/python_bridge.py +476 -0
  111. package/src/cli.ts +331 -0
  112. package/src/config/index.ts +289 -0
  113. package/src/core/analyzer.ts +788 -0
  114. package/src/core/discovery.ts +415 -0
  115. package/src/core/generator.ts +447 -0
  116. package/src/core/mapper.ts +504 -0
  117. package/src/core/validation.ts +693 -0
  118. package/src/index.ts +83 -0
  119. package/src/runtime/base.ts +32 -0
  120. package/src/runtime/errors.ts +19 -0
  121. package/src/runtime/http.ts +132 -0
  122. package/src/runtime/index.ts +24 -0
  123. package/src/runtime/node.ts +431 -0
  124. package/src/runtime/optimized-node.ts +838 -0
  125. package/src/runtime/protocol.ts +2 -0
  126. package/src/runtime/pyodide.ts +228 -0
  127. package/src/types/global.d.ts +60 -0
  128. package/src/types/index.ts +410 -0
  129. package/src/tywrap.ts +669 -0
  130. package/src/utils/bundle-optimizer.ts +876 -0
  131. package/src/utils/cache.ts +634 -0
  132. package/src/utils/codec.ts +275 -0
  133. package/src/utils/logger.ts +201 -0
  134. package/src/utils/memory-profiler.ts +582 -0
  135. package/src/utils/parallel-processor.ts +879 -0
  136. package/src/utils/python.ts +79 -0
  137. 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();