fscr 6.2.3 → 7.3.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/README.md +48 -30
- package/dist/index.js +502 -185
- package/dist/lib/auth/auth-conf.js +49 -45
- package/dist/lib/cache/README.md +341 -0
- package/dist/lib/cache/cli.js +152 -0
- package/dist/lib/cache/file-watcher.js +193 -0
- package/dist/lib/cache/index.js +422 -0
- package/dist/lib/cache/monitor.js +224 -0
- package/dist/lib/commands/doctor.js +225 -0
- package/dist/lib/completions/completion.js +342 -0
- package/dist/lib/completions/generator.js +152 -0
- package/dist/lib/completions/scripts/bash.sh +108 -0
- package/dist/lib/completions/scripts/fish.sh +105 -0
- package/dist/lib/completions/scripts/powershell.ps1 +168 -0
- package/dist/lib/completions/scripts/zsh.sh +124 -0
- package/dist/lib/diagnostics/cache.js +121 -0
- package/dist/lib/diagnostics/fileSystem.js +236 -0
- package/dist/lib/diagnostics/gitCheck.js +41 -0
- package/dist/lib/diagnostics/nodeVersion.js +68 -0
- package/dist/lib/diagnostics/packageManager.js +64 -0
- package/dist/lib/diagnostics/performance.js +141 -0
- package/dist/lib/encryption/decryptConfig.js +3 -2
- package/dist/lib/encryption/encryption.js +153 -113
- package/dist/lib/generators/generateFScripts.js +16 -13
- package/dist/lib/generators/generateToc.js +23 -14
- package/dist/lib/generators/index.js +1 -1
- package/dist/lib/git/pub.js +27 -31
- package/dist/lib/git/taskRunner.js +79 -69
- package/dist/lib/git/validateNotDev.js +65 -54
- package/dist/lib/optionList.js +69 -57
- package/dist/lib/parsers/parseScriptsMd.cached.js +208 -0
- package/dist/lib/parsers/parseScriptsMd.js +88 -79
- package/dist/lib/parsers/parseScriptsPackage.js +4 -3
- package/dist/lib/performance/cache.js +199 -0
- package/dist/lib/performance/lazy-loader.js +189 -0
- package/dist/lib/performance/monitor.js +303 -0
- package/dist/lib/plugins/deployment/index.js +113 -0
- package/dist/lib/plugins/hooks.js +17 -0
- package/dist/lib/plugins/loader.js +91 -0
- package/dist/lib/plugins/task-notifier/index.js +72 -0
- package/dist/lib/release/bump.js +51 -43
- package/dist/lib/release/commitWithMessage.js +80 -52
- package/dist/lib/release/publish.js +19 -14
- package/dist/lib/release/pushToGit.js +40 -31
- package/dist/lib/release/releasenotes.js +116 -97
- package/dist/lib/release/seeChangedFiles.js +68 -60
- package/dist/lib/release/sort.js +200 -116
- package/dist/lib/release/tree.js +161 -147
- package/dist/lib/release/validateNotDev.js +52 -44
- package/dist/lib/running/index.js +1 -1
- package/dist/lib/running/runCLICommand.js +41 -31
- package/dist/lib/running/runParallel.js +61 -59
- package/dist/lib/running/runSequence.js +55 -53
- package/dist/lib/startScripts.js +129 -114
- package/dist/lib/taskList.js +99 -84
- package/dist/lib/test-files/.fscripts.md +113 -0
- package/dist/lib/test-files/.fscripts.test.md +103 -0
- package/dist/lib/test-files/.fscriptsb.md +107 -0
- package/dist/lib/test-files/.mdtest.md +40 -0
- package/dist/lib/test-files/consoleSample.js +17 -0
- package/dist/lib/test-files/inputSample.js +20 -0
- package/dist/lib/test-files/testConsole.js +1 -0
- package/dist/lib/test-files/testInput.js +2 -0
- package/dist/lib/upgradePackages.js +56 -46
- package/dist/lib/utils/clear.js +16 -13
- package/dist/lib/utils/console.js +27 -21
- package/dist/lib/utils/encryption.js +55 -13
- package/dist/lib/utils/hash.js +128 -0
- package/dist/lib/utils/helpers.js +153 -142
- package/dist/lib/utils/index.js +1 -1
- package/dist/lib/utils/prompt.js +24 -29
- package/package.json +20 -32
- package/dist/lib/codemod/arrow.js +0 -13
- package/dist/lib/codemod/arrow2.js +0 -67
- package/dist/lib/codemod/funcs.js +0 -25
- package/dist/lib/codemod/removeConsole.js +0 -12
- package/dist/lib/codemod/test.js +0 -8
- package/dist/lib/components/App.js +0 -64
- package/dist/lib/components/Selector.js +0 -133
- package/dist/lib/components/TabChanger.js +0 -113
- package/dist/lib/components/Table.js +0 -177
- package/dist/lib/components/Tabs.js +0 -221
- package/dist/lib/generateFScripts.js +0 -25
- package/dist/lib/generateToc.js +0 -30
- package/dist/lib/helpers.js +0 -191
- package/dist/lib/parseScriptsMd.js +0 -85
- package/dist/lib/parseScriptsPackage.js +0 -9
- package/dist/lib/release/index.js +0 -4
- package/dist/lib/run/lib.js +0 -454
- package/dist/lib/run/main-p.js +0 -59
- package/dist/lib/run/main-s.js +0 -56
- package/dist/lib/run/parse-cli-args.js +0 -222
- package/dist/lib/run/run-p.js +0 -30
- package/dist/lib/run/run-s.js +0 -57
- package/dist/lib/runCLICommand.js +0 -30
- package/dist/lib/runParallel.js +0 -20
- package/dist/lib/runSequence.js +0 -38
- package/dist/lib/taskListAutoComplete.js +0 -15
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File watcher for automatic cache invalidation
|
|
3
|
+
* @module cache/file-watcher
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { watch } from 'fs';
|
|
7
|
+
import { resolve } from 'path';
|
|
8
|
+
import EventEmitter from 'events';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* FileWatcher - Monitors files and triggers cache invalidation
|
|
12
|
+
* @class
|
|
13
|
+
* @extends EventEmitter
|
|
14
|
+
*/
|
|
15
|
+
class FileWatcher extends EventEmitter {
|
|
16
|
+
/**
|
|
17
|
+
* Create a file watcher
|
|
18
|
+
* @param {CacheManager} cacheManager - Cache manager instance
|
|
19
|
+
* @param {Object} options - Configuration options
|
|
20
|
+
* @param {number} [options.debounceMs=100] - Debounce time in milliseconds
|
|
21
|
+
*/
|
|
22
|
+
constructor(cacheManager, options = {}) {
|
|
23
|
+
super();
|
|
24
|
+
|
|
25
|
+
this.cacheManager = cacheManager;
|
|
26
|
+
this.debounceMs = options.debounceMs || 100;
|
|
27
|
+
this.watchers = new Map();
|
|
28
|
+
this.debounceTimers = new Map();
|
|
29
|
+
|
|
30
|
+
// Statistics
|
|
31
|
+
this.stats = {
|
|
32
|
+
filesWatched: 0,
|
|
33
|
+
changeEvents: 0,
|
|
34
|
+
invalidations: 0
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Watch a file for changes
|
|
40
|
+
* @param {string} filePath - Path to file
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
watch(filePath) {
|
|
44
|
+
const absolutePath = resolve(filePath);
|
|
45
|
+
|
|
46
|
+
// Don't watch if already watching
|
|
47
|
+
if (this.watchers.has(absolutePath)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const watcher = watch(absolutePath, (eventType) => {
|
|
53
|
+
this._handleChange(absolutePath, eventType);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// unref() so watching a file never keeps a one-shot CLI process alive.
|
|
57
|
+
if (typeof watcher.unref === 'function') {
|
|
58
|
+
watcher.unref();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
watcher.on('error', (error) => {
|
|
62
|
+
console.warn(`File watcher error for ${absolutePath}:`, error.message);
|
|
63
|
+
this.unwatch(absolutePath);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.watchers.set(absolutePath, watcher);
|
|
67
|
+
this.stats.filesWatched++;
|
|
68
|
+
|
|
69
|
+
this.emit('watch', { filePath: absolutePath });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.warn(`Failed to watch file ${absolutePath}:`, error.message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Stop watching a file
|
|
77
|
+
* @param {string} filePath - Path to file
|
|
78
|
+
* @returns {boolean} True if file was being watched
|
|
79
|
+
*/
|
|
80
|
+
unwatch(filePath) {
|
|
81
|
+
const absolutePath = resolve(filePath);
|
|
82
|
+
const watcher = this.watchers.get(absolutePath);
|
|
83
|
+
|
|
84
|
+
if (watcher) {
|
|
85
|
+
watcher.close();
|
|
86
|
+
this.watchers.delete(absolutePath);
|
|
87
|
+
this.stats.filesWatched--;
|
|
88
|
+
this.emit('unwatch', { filePath: absolutePath });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handle file change event
|
|
97
|
+
* @private
|
|
98
|
+
* @param {string} filePath - Path to changed file
|
|
99
|
+
* @param {string} eventType - Type of change event
|
|
100
|
+
*/
|
|
101
|
+
_handleChange(filePath, eventType) {
|
|
102
|
+
// Clear existing debounce timer
|
|
103
|
+
if (this.debounceTimers.has(filePath)) {
|
|
104
|
+
clearTimeout(this.debounceTimers.get(filePath));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Debounce the invalidation
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
this._invalidateFile(filePath, eventType);
|
|
110
|
+
this.debounceTimers.delete(filePath);
|
|
111
|
+
}, this.debounceMs);
|
|
112
|
+
if (typeof timer.unref === 'function') {
|
|
113
|
+
timer.unref();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.debounceTimers.set(filePath, timer);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Invalidate cache entries for a file
|
|
121
|
+
* @private
|
|
122
|
+
* @param {string} filePath - Path to file
|
|
123
|
+
* @param {string} eventType - Type of change event
|
|
124
|
+
*/
|
|
125
|
+
_invalidateFile(filePath, eventType) {
|
|
126
|
+
this.stats.changeEvents++;
|
|
127
|
+
|
|
128
|
+
const invalidated = this.cacheManager.invalidateFile(filePath);
|
|
129
|
+
|
|
130
|
+
if (invalidated > 0) {
|
|
131
|
+
this.stats.invalidations += invalidated;
|
|
132
|
+
this.emit('invalidate', {
|
|
133
|
+
filePath,
|
|
134
|
+
eventType,
|
|
135
|
+
count: invalidated
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Stop watching all files
|
|
142
|
+
* @returns {void}
|
|
143
|
+
*/
|
|
144
|
+
unwatchAll() {
|
|
145
|
+
for (const [filePath, watcher] of this.watchers.entries()) {
|
|
146
|
+
watcher.close();
|
|
147
|
+
}
|
|
148
|
+
this.watchers.clear();
|
|
149
|
+
this.stats.filesWatched = 0;
|
|
150
|
+
|
|
151
|
+
// Clear all debounce timers
|
|
152
|
+
for (const timer of this.debounceTimers.values()) {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
}
|
|
155
|
+
this.debounceTimers.clear();
|
|
156
|
+
|
|
157
|
+
this.emit('unwatchAll');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get watcher statistics
|
|
162
|
+
* @returns {Object} Statistics object
|
|
163
|
+
*/
|
|
164
|
+
getStats() {
|
|
165
|
+
return {
|
|
166
|
+
filesWatched: this.stats.filesWatched,
|
|
167
|
+
changeEvents: this.stats.changeEvents,
|
|
168
|
+
invalidations: this.stats.invalidations,
|
|
169
|
+
watchedFiles: Array.from(this.watchers.keys())
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if a file is being watched
|
|
175
|
+
* @param {string} filePath - Path to file
|
|
176
|
+
* @returns {boolean} True if being watched
|
|
177
|
+
*/
|
|
178
|
+
isWatching(filePath) {
|
|
179
|
+
const absolutePath = resolve(filePath);
|
|
180
|
+
return this.watchers.has(absolutePath);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Destroy the file watcher and cleanup resources
|
|
185
|
+
* @returns {void}
|
|
186
|
+
*/
|
|
187
|
+
destroy() {
|
|
188
|
+
this.unwatchAll();
|
|
189
|
+
this.removeAllListeners();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export default FileWatcher;
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Manager with TTL support
|
|
3
|
+
* Provides in-memory caching with automatic invalidation
|
|
4
|
+
* @module cache
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
import { statSync } from 'fs';
|
|
9
|
+
import EventEmitter from 'events';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Cache entry structure
|
|
13
|
+
* @typedef {Object} CacheEntry
|
|
14
|
+
* @property {*} data - Cached data
|
|
15
|
+
* @property {number} timestamp - Creation timestamp
|
|
16
|
+
* @property {number} ttl - Time to live in milliseconds
|
|
17
|
+
* @property {string} key - Cache key
|
|
18
|
+
* @property {string} [filePath] - Associated file path for file-based caching
|
|
19
|
+
* @property {number} [mtime] - File modification time
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Cache statistics
|
|
24
|
+
* @typedef {Object} CacheStats
|
|
25
|
+
* @property {number} hits - Cache hit count
|
|
26
|
+
* @property {number} misses - Cache miss count
|
|
27
|
+
* @property {number} invalidations - Cache invalidation count
|
|
28
|
+
* @property {number} size - Current cache size
|
|
29
|
+
* @property {number} hitRate - Cache hit rate percentage
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* CacheManager - In-memory cache with TTL and file watching
|
|
34
|
+
* @class
|
|
35
|
+
* @extends EventEmitter
|
|
36
|
+
*/
|
|
37
|
+
class CacheManager extends EventEmitter {
|
|
38
|
+
/**
|
|
39
|
+
* Create a cache manager
|
|
40
|
+
* @param {Object} options - Configuration options
|
|
41
|
+
* @param {number} [options.defaultTTL=300000] - Default TTL in milliseconds (5 minutes)
|
|
42
|
+
* @param {number} [options.maxSize=100] - Maximum number of cache entries
|
|
43
|
+
* @param {boolean} [options.enableStats=true] - Enable statistics tracking
|
|
44
|
+
*/
|
|
45
|
+
constructor(options = {}) {
|
|
46
|
+
super();
|
|
47
|
+
|
|
48
|
+
this.cache = new Map();
|
|
49
|
+
this.defaultTTL = options.defaultTTL || 300000; // 5 minutes
|
|
50
|
+
this.maxSize = options.maxSize || 100;
|
|
51
|
+
this.enableStats = options.enableStats !== false;
|
|
52
|
+
|
|
53
|
+
// Statistics
|
|
54
|
+
this.stats = {
|
|
55
|
+
hits: 0,
|
|
56
|
+
misses: 0,
|
|
57
|
+
invalidations: 0,
|
|
58
|
+
evictions: 0,
|
|
59
|
+
startTime: Date.now()
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Cleanup interval - run every minute.
|
|
63
|
+
// unref() so this background timer never keeps a one-shot CLI process alive.
|
|
64
|
+
this.cleanupInterval = setInterval(() => this._cleanup(), 60000);
|
|
65
|
+
if (this.cleanupInterval && typeof this.cleanupInterval.unref === "function") {
|
|
66
|
+
this.cleanupInterval.unref();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Ensure cleanup on exit
|
|
70
|
+
process.on('exit', () => this.destroy());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generate a cache key from file path and modification time
|
|
75
|
+
* @param {string} filePath - File path
|
|
76
|
+
* @returns {string} Cache key
|
|
77
|
+
*/
|
|
78
|
+
generateFileKey(filePath) {
|
|
79
|
+
try {
|
|
80
|
+
const stats = statSync(filePath);
|
|
81
|
+
const mtime = stats.mtimeMs;
|
|
82
|
+
return this._hash(`${filePath}:${mtime}`);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new Error(`Failed to generate cache key for ${filePath}: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generate a hash-based cache key
|
|
90
|
+
* @private
|
|
91
|
+
* @param {string} input - Input string
|
|
92
|
+
* @returns {string} Hash
|
|
93
|
+
*/
|
|
94
|
+
_hash(input) {
|
|
95
|
+
return createHash('sha256').update(input).digest('hex').substring(0, 16);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get value from cache
|
|
100
|
+
* @template T
|
|
101
|
+
* @param {string} key - Cache key
|
|
102
|
+
* @returns {T|null} Cached value or null
|
|
103
|
+
*/
|
|
104
|
+
get(key) {
|
|
105
|
+
const entry = this.cache.get(key);
|
|
106
|
+
|
|
107
|
+
if (!entry) {
|
|
108
|
+
this._recordMiss();
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check if entry is expired
|
|
113
|
+
if (this._isExpired(entry)) {
|
|
114
|
+
this.invalidate(key);
|
|
115
|
+
this._recordMiss();
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check if file-based cache is still valid
|
|
120
|
+
if (entry.filePath) {
|
|
121
|
+
try {
|
|
122
|
+
const stats = statSync(entry.filePath);
|
|
123
|
+
if (stats.mtimeMs !== entry.mtime) {
|
|
124
|
+
this.invalidate(key);
|
|
125
|
+
this._recordMiss();
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// File might have been deleted
|
|
130
|
+
this.invalidate(key);
|
|
131
|
+
this._recordMiss();
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this._recordHit();
|
|
137
|
+
this.emit('hit', { key, data: entry.data });
|
|
138
|
+
return entry.data;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Set value in cache
|
|
143
|
+
* @template T
|
|
144
|
+
* @param {string} key - Cache key
|
|
145
|
+
* @param {T} data - Data to cache
|
|
146
|
+
* @param {Object} [options] - Cache options
|
|
147
|
+
* @param {number} [options.ttl] - Custom TTL in milliseconds
|
|
148
|
+
* @param {string} [options.filePath] - Associated file path
|
|
149
|
+
* @returns {void}
|
|
150
|
+
*/
|
|
151
|
+
set(key, data, options = {}) {
|
|
152
|
+
// Evict oldest entry if cache is full
|
|
153
|
+
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
|
|
154
|
+
this._evictOldest();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const ttl = options.ttl !== undefined ? options.ttl : this.defaultTTL;
|
|
158
|
+
const entry = {
|
|
159
|
+
data,
|
|
160
|
+
timestamp: Date.now(),
|
|
161
|
+
ttl,
|
|
162
|
+
key
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Add file tracking if provided
|
|
166
|
+
if (options.filePath) {
|
|
167
|
+
try {
|
|
168
|
+
const stats = statSync(options.filePath);
|
|
169
|
+
entry.filePath = options.filePath;
|
|
170
|
+
entry.mtime = stats.mtimeMs;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.warn(`Warning: Could not stat file ${options.filePath}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.cache.set(key, entry);
|
|
177
|
+
this.emit('set', { key, data, ttl });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Invalidate a cache entry
|
|
182
|
+
* @param {string} key - Cache key
|
|
183
|
+
* @returns {boolean} True if entry was deleted
|
|
184
|
+
*/
|
|
185
|
+
invalidate(key) {
|
|
186
|
+
const deleted = this.cache.delete(key);
|
|
187
|
+
if (deleted) {
|
|
188
|
+
this.stats.invalidations++;
|
|
189
|
+
this.emit('invalidate', { key });
|
|
190
|
+
}
|
|
191
|
+
return deleted;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Invalidate all entries for a specific file path
|
|
196
|
+
* @param {string} filePath - File path
|
|
197
|
+
* @returns {number} Number of invalidated entries
|
|
198
|
+
*/
|
|
199
|
+
invalidateFile(filePath) {
|
|
200
|
+
let count = 0;
|
|
201
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
202
|
+
if (entry.filePath === filePath) {
|
|
203
|
+
this.invalidate(key);
|
|
204
|
+
count++;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return count;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Clear all cache entries
|
|
212
|
+
* @returns {void}
|
|
213
|
+
*/
|
|
214
|
+
clear() {
|
|
215
|
+
const size = this.cache.size;
|
|
216
|
+
this.cache.clear();
|
|
217
|
+
this.stats.invalidations += size;
|
|
218
|
+
this.emit('clear');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if a cache entry is valid
|
|
223
|
+
* @param {string} key - Cache key
|
|
224
|
+
* @returns {boolean} True if valid
|
|
225
|
+
*/
|
|
226
|
+
isValid(key) {
|
|
227
|
+
const entry = this.cache.get(key);
|
|
228
|
+
if (!entry) return false;
|
|
229
|
+
|
|
230
|
+
if (this._isExpired(entry)) return false;
|
|
231
|
+
|
|
232
|
+
// Check file modification time if applicable
|
|
233
|
+
if (entry.filePath) {
|
|
234
|
+
try {
|
|
235
|
+
const stats = statSync(entry.filePath);
|
|
236
|
+
return stats.mtimeMs === entry.mtime;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if an entry is expired
|
|
247
|
+
* @private
|
|
248
|
+
* @param {CacheEntry} entry - Cache entry
|
|
249
|
+
* @returns {boolean} True if expired
|
|
250
|
+
*/
|
|
251
|
+
_isExpired(entry) {
|
|
252
|
+
if (entry.ttl === 0) return false; // TTL of 0 means never expire
|
|
253
|
+
return Date.now() - entry.timestamp > entry.ttl;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get cache statistics
|
|
258
|
+
* @returns {CacheStats} Statistics object
|
|
259
|
+
*/
|
|
260
|
+
getStats() {
|
|
261
|
+
const total = this.stats.hits + this.stats.misses;
|
|
262
|
+
const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
|
|
263
|
+
const uptime = Date.now() - this.stats.startTime;
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
hits: this.stats.hits,
|
|
267
|
+
misses: this.stats.misses,
|
|
268
|
+
invalidations: this.stats.invalidations,
|
|
269
|
+
evictions: this.stats.evictions,
|
|
270
|
+
size: this.cache.size,
|
|
271
|
+
maxSize: this.maxSize,
|
|
272
|
+
hitRate: hitRate.toFixed(2),
|
|
273
|
+
uptime,
|
|
274
|
+
totalRequests: total
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Reset statistics
|
|
280
|
+
* @returns {void}
|
|
281
|
+
*/
|
|
282
|
+
resetStats() {
|
|
283
|
+
this.stats = {
|
|
284
|
+
hits: 0,
|
|
285
|
+
misses: 0,
|
|
286
|
+
invalidations: 0,
|
|
287
|
+
evictions: 0,
|
|
288
|
+
startTime: Date.now()
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Cleanup expired entries
|
|
294
|
+
* @private
|
|
295
|
+
* @returns {number} Number of cleaned entries
|
|
296
|
+
*/
|
|
297
|
+
_cleanup() {
|
|
298
|
+
let cleaned = 0;
|
|
299
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
300
|
+
if (this._isExpired(entry)) {
|
|
301
|
+
this.cache.delete(key);
|
|
302
|
+
cleaned++;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (cleaned > 0) {
|
|
307
|
+
this.emit('cleanup', { count: cleaned });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return cleaned;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Evict the oldest cache entry
|
|
315
|
+
* @private
|
|
316
|
+
* @returns {void}
|
|
317
|
+
*/
|
|
318
|
+
_evictOldest() {
|
|
319
|
+
let oldestKey = null;
|
|
320
|
+
let oldestTime = Infinity;
|
|
321
|
+
|
|
322
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
323
|
+
if (entry.timestamp < oldestTime) {
|
|
324
|
+
oldestTime = entry.timestamp;
|
|
325
|
+
oldestKey = key;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (oldestKey) {
|
|
330
|
+
this.cache.delete(oldestKey);
|
|
331
|
+
this.stats.evictions++;
|
|
332
|
+
this.emit('evict', { key: oldestKey });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Record a cache hit
|
|
338
|
+
* @private
|
|
339
|
+
*/
|
|
340
|
+
_recordHit() {
|
|
341
|
+
if (this.enableStats) {
|
|
342
|
+
this.stats.hits++;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Record a cache miss
|
|
348
|
+
* @private
|
|
349
|
+
*/
|
|
350
|
+
_recordMiss() {
|
|
351
|
+
if (this.enableStats) {
|
|
352
|
+
this.stats.misses++;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Destroy the cache manager and cleanup resources
|
|
358
|
+
* @returns {void}
|
|
359
|
+
*/
|
|
360
|
+
destroy() {
|
|
361
|
+
if (this.cleanupInterval) {
|
|
362
|
+
clearInterval(this.cleanupInterval);
|
|
363
|
+
this.cleanupInterval = null;
|
|
364
|
+
}
|
|
365
|
+
this.clear();
|
|
366
|
+
this.removeAllListeners();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Get all cache keys
|
|
371
|
+
* @returns {string[]} Array of cache keys
|
|
372
|
+
*/
|
|
373
|
+
keys() {
|
|
374
|
+
return Array.from(this.cache.keys());
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Get cache size
|
|
379
|
+
* @returns {number} Number of entries
|
|
380
|
+
*/
|
|
381
|
+
size() {
|
|
382
|
+
return this.cache.size;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Check if cache has a key
|
|
387
|
+
* @param {string} key - Cache key
|
|
388
|
+
* @returns {boolean} True if key exists
|
|
389
|
+
*/
|
|
390
|
+
has(key) {
|
|
391
|
+
return this.cache.has(key) && this.isValid(key);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Singleton instance
|
|
396
|
+
let globalCache = null;
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Get the global cache instance
|
|
400
|
+
* @param {Object} [options] - Configuration options
|
|
401
|
+
* @returns {CacheManager} Cache manager instance
|
|
402
|
+
*/
|
|
403
|
+
export function getCache(options) {
|
|
404
|
+
if (!globalCache) {
|
|
405
|
+
globalCache = new CacheManager(options);
|
|
406
|
+
}
|
|
407
|
+
return globalCache;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Reset the global cache instance
|
|
412
|
+
* @returns {void}
|
|
413
|
+
*/
|
|
414
|
+
export function resetCache() {
|
|
415
|
+
if (globalCache) {
|
|
416
|
+
globalCache.destroy();
|
|
417
|
+
globalCache = null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export { CacheManager };
|
|
422
|
+
export default CacheManager;
|