fortify2-js
Version:
MOST POWERFUL JavaScript Security Library! Military-grade cryptography + 19 enhanced object methods + quantum-resistant algorithms + perfect TypeScript support. More powerful than Lodash with built-in security.
757 lines (751 loc) • 27.2 kB
JavaScript
'use strict';
var fs = require('fs');
var path = require('path');
var events = require('events');
var crypto = require('crypto');
var FileWatcher_config = require('../../const/FileWatcher.config.js');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto);
/**
* Ultra-Fast File Watcher for Auto-Reload
* High-performance alternative to nodemon with advanced features
*/
class UltraFastFileWatcher extends events.EventEmitter {
constructor(config = {}) {
super();
this.watchers = new Map();
this.debounceTimers = new Map();
this.pendingChanges = [];
this.fileHashes = new Map();
this.isWatching = false;
this.startTime = Date.now();
this.customIgnorePatterns = [];
this.lastRestartTime = 0;
this.setMaxListeners(50); // Increase max listeners for better performance
this.config = {
...FileWatcher_config.DEFAULT_FW_CONFIG,
...config,
};
this.restartStats = {
totalRestarts: 0,
lastRestart: null,
averageRestartTime: 0,
fastestRestart: Infinity,
slowestRestart: 0,
successfulRestarts: 0,
failedRestarts: 0,
restartHistory: [],
};
this.health = {
isHealthy: true,
uptime: 0,
memoryUsage: process.memoryUsage(),
activeConnections: 0,
lastHealthCheck: new Date(),
errors: [],
};
this.setupProcessHandlers();
this.loadCustomIgnorePatterns();
}
/**
* Setup process handlers for graceful shutdown
*/
setupProcessHandlers() {
const gracefulShutdown = async (signal) => {
await this.stopWatching();
process.exit(0);
};
process.on("SIGINT", () => gracefulShutdown());
process.on("SIGTERM", () => gracefulShutdown());
process.on("uncaughtException", (error) => {
console.error("Uncaught Exception:", error);
this.logError("Uncaught Exception", error.message);
});
process.on("unhandledRejection", (reason) => {
console.error("Unhandled Rejection:", reason);
this.logError("Unhandled Rejection", String(reason));
});
}
/**
* Load custom ignore patterns from file
*/
async loadCustomIgnorePatterns() {
try {
const ignoreFile = path.join(process.cwd(), this.config.customIgnoreFile);
const content = await fs.promises.readFile(ignoreFile, "utf8");
this.customIgnorePatterns = content
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"))
.map((pattern) => {
try {
// Convert glob-like patterns to regex
const regexPattern = pattern
.replace(/\./g, "\\.")
.replace(/\*/g, ".*")
.replace(/\?/g, ".");
return new RegExp(regexPattern);
}
catch {
return null;
}
})
.filter(Boolean);
if (this.config.verbose) {
console.log(`Loaded ${this.customIgnorePatterns.length} custom ignore patterns`);
}
}
catch {
// Ignore if file doesn't exist
}
}
/**
* Start watching files with initialization
*/
async startWatching(restartCallback) {
if (!this.config.enabled) {
console.log("📴 File watcher disabled");
return;
}
if (this.isWatching) {
console.log("File watcher already running");
return;
}
this.restartCallback = restartCallback;
this.startTime = Date.now();
try {
if (this.config.clearScreen) {
console.clear();
}
if (this.config.showBanner) {
this.showBanner();
}
// console.log("Starting file watcher...");
// Validate watch paths
await this.validateWatchPaths();
// Start watching directories
const watchPromises = this.config.watchPaths.map((path) => this.watchDirectory(path));
if (this.config.parallelProcessing) {
await Promise.allSettled(watchPromises);
}
else {
for (const promise of watchPromises) {
await promise;
}
}
this.isWatching = true;
// Start health check
if (this.config.healthCheck) {
this.startHealthCheck();
}
this.emit("watcher:started", {
watchPaths: this.config.watchPaths,
extensions: this.config.extensions,
config: this.config,
});
// console.log(`File watcher started successfully!`);
// console.log(`Watching ${this.config.watchPaths.length} paths`);
// console.log(`Extensions: ${this.config.extensions.join(", ")}`);
// console.log(` Debounce: ${this.config.debounceMs}ms`);
// console.log(
// ` Batch changes: ${
// this.config.batchChanges ? "enabled" : "disabled"
// }`
// );
if (this.config.verbose) {
console.log(` Paths: ${this.config.watchPaths.join(", ")}`);
console.log(` Ignored: ${this.config.ignorePaths.length} paths, ${this.config.ignorePatterns.length +
this.customIgnorePatterns.length} patterns`);
}
}
catch (error) {
console.error("Failed to start file watcher:", error.message);
this.logError("Start watcher failed", error.message);
throw error;
}
}
/**
* directory watching with better error handling
*/
async watchDirectory(watchPath) {
try {
const fullPath = path.resolve(process.cwd(), watchPath);
// Check if path exists
const stats = await fs.promises.stat(fullPath).catch(() => null);
if (!stats) {
console.warn(`Path does not exist: ${watchPath}`);
return;
}
if (!stats.isDirectory()) {
console.warn(`Path is not a directory: ${watchPath}`);
return;
}
const watcher = fs.watch(fullPath, {
recursive: true,
persistent: this.config.persistentWatching,
}, async (eventType, filename) => {
if (!filename)
return;
await this.handleWatchEvent(eventType, filename, fullPath);
});
watcher.on("error", (error) => {
console.error(`Watcher error for ${watchPath}:`, error.message);
this.logError(`Watcher error: ${watchPath}`, error.message);
// Attempt to restart this watcher
setTimeout(() => this.restartWatcher(watchPath), 1000);
});
watcher.on("close", () => {
if (this.config.verbose) {
console.log(`Watcher closed for: ${watchPath}`);
}
});
this.watchers.set(watchPath, watcher);
this.health.activeConnections++;
if (this.config.verbose) {
console.log(`Watching: ${watchPath}`);
}
}
catch (error) {
console.error(`Failed to watch directory ${watchPath}:`, error.message);
this.logError(`Watch directory failed: ${watchPath}`, error.message);
}
}
/**
* Handle watch events with processing
*/
async handleWatchEvent(eventType, filename, basePath) {
try {
const filePath = path.join(basePath, filename);
const relativePath = path.relative(process.cwd(), filePath);
// Skip if should be ignored
if (this.shouldIgnoreFile(relativePath, filename)) {
return;
}
// Get file stats
let stats = null;
let isDirectory = false;
let size = 0;
try {
stats = await fs.promises.stat(filePath);
isDirectory = stats.isDirectory();
size = stats.size;
// Skip large files
if (!isDirectory &&
size > this.config.maxFileSize * 1024 * 1024) {
if (this.config.verbose) {
console.log(`Skipping large file: ${filename} (${(size /
1024 /
1024).toFixed(2)}MB)`);
}
return;
}
// Skip empty files if configured
if (this.config.excludeEmptyFiles &&
size === 0 &&
!isDirectory) {
return;
}
}
catch {
// File might have been deleted
}
// Check file extension for non-directories
if (!isDirectory && !this.shouldWatchFile(filename)) {
return;
}
// Generate hash for content comparison if enabled
let hash;
let previousHash;
if (this.config.enableFileHashing && !isDirectory && stats) {
try {
const content = await fs.promises.readFile(filePath);
hash = crypto__namespace
.createHash("md5")
.update(content)
.digest("hex");
previousHash = this.fileHashes.get(filePath);
// Skip if content hasn't actually changed
if (previousHash === hash) {
return;
}
this.fileHashes.set(filePath, hash);
}
catch {
// Ignore hash errors
}
}
const changeEvent = {
type: this.getEventType(eventType, stats !== null),
filename: path.basename(filename),
fullPath: filePath,
relativePath,
timestamp: new Date(),
size,
hash,
previousHash,
isDirectory,
stats: stats || undefined,
};
if (this.config.batchChanges) {
this.addToBatch(changeEvent);
}
else {
this.handleFileChange(changeEvent);
}
}
catch (error) {
if (this.config.verbose) {
console.error(`Error handling watch event:`, error.message);
}
}
}
/**
* Add change to batch processing
*/
addToBatch(event) {
this.pendingChanges.push(event);
if (this.batchTimer) {
clearTimeout(this.batchTimer);
}
this.batchTimer = setTimeout(() => {
this.processBatch();
}, this.config.batchTimeout);
}
/**
* Process batched changes
*/
processBatch() {
if (this.pendingChanges.length === 0)
return;
const startTime = Date.now();
const changes = [...this.pendingChanges];
this.pendingChanges = [];
const batchEvent = {
changes,
totalFiles: changes.length,
timestamp: new Date(),
duration: 0,
};
if (this.config.verbose) {
console.log(`Processing batch of ${changes.length} changes`);
}
// Process the most recent change for restart
const latestChange = changes[changes.length - 1];
this.processFileChange(latestChange, changes.length);
batchEvent.duration = Date.now() - startTime;
this.emit("batch:processed", batchEvent);
}
/**
* file change handling
*/
handleFileChange(event) {
const key = event.fullPath;
// Clear existing timer
const existingTimer = this.debounceTimers.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
}
// Set debounced timer
const timer = setTimeout(() => {
this.processFileChange(event);
this.debounceTimers.delete(key);
}, this.config.debounceMs);
this.debounceTimers.set(key, timer);
}
/**
* Process file change with logic
*/
async processFileChange(event, batchSize = 1) {
try {
if (this.config.verbose) {
const batchInfo = batchSize > 1 ? ` (batch of ${batchSize})` : "";
console.log(`File ${event.type}: ${event.relativePath}${batchInfo}`);
}
this.emit("file:changed", event);
// Check restart limits with time-based reset
const now = Date.now();
if (now - this.lastRestartTime > this.config.resetRestartsAfter) {
this.restartStats.totalRestarts = 0;
this.lastRestartTime = now;
}
if (this.restartStats.totalRestarts >= this.config.maxRestarts) {
console.warn(`Maximum restarts (${this.config.maxRestarts}) reached. Auto-reload paused.`);
console.warn(` Will reset after ${Math.round(this.config.resetRestartsAfter / 60000)} minutes.`);
return;
}
// Trigger restart
await this.triggerRestart(event, batchSize);
}
catch (error) {
console.error("Failed to process file change:", error.message);
this.logError("Process file change failed", error.message);
}
}
/**
* server restart with better error handling
*/
async triggerRestart(event, fileCount = 1) {
const startTime = Date.now();
const memoryBefore = process.memoryUsage();
try {
const fileInfo = fileCount > 1 ? ` (${fileCount} files)` : "";
console.log(`Restarting server due to changes ${fileInfo}: ${event.relativePath}`);
this.emit("restart:starting", { event, fileCount });
// Graceful shutdown of previous process
if (this.childProcess && this.config.gracefulShutdown) {
await this.gracefulShutdown();
}
// Add restart delay
if (this.config.restartDelay > 0) {
await new Promise((resolve) => setTimeout(resolve, this.config.restartDelay));
}
// Execute restart callback
if (this.restartCallback) {
await Promise.race([
this.restartCallback(),
new Promise((_, reject) => setTimeout(() => reject(new Error("Restart timeout")), 30000)),
]);
}
const duration = Date.now() - startTime;
this.updateRestartStats(event.relativePath, duration, true, fileCount, memoryBefore);
this.emit("restart:completed", {
event,
duration,
success: true,
fileCount,
});
// const speedEmoji =
// duration < 500 ? "⚡" : duration < 1000 ? "" : "🔄";
//${speedEmoji}
console.log(` Server restarted successfully (${duration}ms)`);
}
catch (error) {
const duration = Date.now() - startTime;
this.updateRestartStats(event.relativePath, duration, false, fileCount, memoryBefore);
this.emit("restart:failed", {
event,
error: error.message,
duration,
fileCount,
});
console.error(`Server restart failed (${duration}ms): ${error.message}`);
this.logError("Server restart failed", error.message);
}
}
/**
* Graceful shutdown of child process
*/
async gracefulShutdown() {
if (!this.childProcess)
return;
return new Promise((resolve) => {
const timeout = setTimeout(() => {
if (this.childProcess) {
this.childProcess.kill("SIGKILL");
}
resolve();
}, this.config.gracefulShutdownTimeout);
this.childProcess.on("exit", () => {
clearTimeout(timeout);
resolve();
});
this.childProcess.kill("SIGTERM");
});
}
/**
* restart statistics
*/
updateRestartStats(filename, duration, success, fileCount, memoryUsage) {
this.restartStats.totalRestarts++;
this.restartStats.lastRestart = new Date();
if (success) {
this.restartStats.successfulRestarts++;
this.restartStats.fastestRestart = Math.min(this.restartStats.fastestRestart, duration);
this.restartStats.slowestRestart = Math.max(this.restartStats.slowestRestart, duration);
}
else {
this.restartStats.failedRestarts++;
}
this.restartStats.restartHistory.push({
timestamp: new Date(),
reason: `File changed: ${filename}`,
duration,
success,
fileCount,
memoryUsage,
});
// Keep only last 50 restart records
if (this.restartStats.restartHistory.length > 50) {
this.restartStats.restartHistory =
this.restartStats.restartHistory.slice(-50);
}
// Calculate average restart time (successful only)
const successfulRestarts = this.restartStats.restartHistory.filter((r) => r.success);
if (successfulRestarts.length > 0) {
const totalDuration = successfulRestarts.reduce((sum, r) => sum + r.duration, 0);
this.restartStats.averageRestartTime =
totalDuration / successfulRestarts.length;
}
}
/**
* file filtering with pattern matching
*/
shouldIgnoreFile(filePath, filename) {
// Check ignore paths
if (this.config.ignorePaths.some((ignorePath) => filePath.includes(ignorePath) ||
filePath.startsWith(ignorePath))) {
return true;
}
// Check ignore patterns
if (this.config.ignorePatterns.some((pattern) => pattern.test(filePath))) {
return true;
}
// Check custom ignore patterns
if (this.customIgnorePatterns.some((pattern) => pattern.test(filePath))) {
return true;
}
// Check dot files
if (!this.config.watchDotFiles &&
filename.startsWith(".") &&
filename !== ".env") {
return true;
}
return false;
}
/**
* file extension checking
*/
shouldWatchFile(filename) {
const ext = path.extname(filename).toLowerCase();
return (this.config.extensions.length === 0 ||
this.config.extensions.includes(ext));
}
/**
* Get proper event type
*/
getEventType(eventType, fileExists) {
switch (eventType) {
case "change":
return "change";
case "rename":
return fileExists ? "add" : "delete";
default:
return "change";
}
}
/**
* Validate watch paths
*/
async validateWatchPaths() {
const validPaths = [];
for (const path$1 of this.config.watchPaths) {
try {
const fullPath = path.resolve(process.cwd(), path$1);
const stats = await fs.promises.stat(fullPath);
if (stats.isDirectory()) {
validPaths.push(path$1);
}
else {
console.warn(`Skipping non-directory path: ${path$1}`);
}
}
catch {
// console.warn(`Skipping non-existent path: ${path}`);
}
}
if (validPaths.length === 0) {
throw new Error("No valid watch paths found");
}
this.config.watchPaths = validPaths;
}
/**
* Restart a specific watcher
*/
async restartWatcher(watchPath) {
if (!this.isWatching)
return;
try {
console.log(`Restarting watcher for: ${watchPath}`);
const existingWatcher = this.watchers.get(watchPath);
if (existingWatcher) {
existingWatcher.close();
this.watchers.delete(watchPath);
this.health.activeConnections--;
}
await this.watchDirectory(watchPath);
console.log(`Watcher restarted for: ${watchPath}`);
}
catch (error) {
console.error(`Failed to restart watcher for ${watchPath}:`, error.message);
this.logError(`Restart watcher failed: ${watchPath}`, error.message);
}
}
/**
* Start health monitoring
*/
startHealthCheck() {
this.healthCheckTimer = setInterval(() => {
this.performHealthCheck();
}, this.config.healthCheckInterval);
}
/**
* Perform health check
*/
performHealthCheck() {
const now = Date.now();
const memoryUsage = process.memoryUsage();
const memoryMB = memoryUsage.heapUsed / 1024 / 1024;
this.health.uptime = now - this.startTime;
this.health.memoryUsage = memoryUsage;
this.health.lastHealthCheck = new Date();
this.health.isHealthy = memoryMB < this.config.memoryLimit;
if (!this.health.isHealthy) {
console.warn(`High memory usage: ${memoryMB.toFixed(2)}MB (limit: ${this.config.memoryLimit}MB)`);
this.logError("High memory usage", `${memoryMB.toFixed(2)}MB`);
}
this.emit("health:check", this.health);
if (this.config.verbose) {
console.log(`Health check: ${this.health.isHealthy ? "OK" : "WARNING"} (${memoryMB.toFixed(2)}MB)`);
}
}
/**
* Log error to health system
*/
logError(type, message) {
this.health.errors.push({
timestamp: new Date(),
error: `${type}: ${message}`,
resolved: false,
});
// Keep only last 20 errors
if (this.health.errors.length > 20) {
this.health.errors = this.health.errors.slice(-20);
}
}
/**
* Show startup banner
*/
showBanner() {
// console.log("╔════════════════════════════════════════╗");
// console.log("║ UF - Watcher ║");
// console.log("║ High-Performance File Monitor ║");
// console.log("║ Powered By Nehonix ║");
// console.log("╚════════════════════════════════════════╝");
// console.log("");
}
/**
* stop watching with cleanup
*/
async stopWatching() {
if (!this.isWatching)
return;
try {
console.log("Stopping file watcher...");
// Stop health check
if (this.healthCheckTimer) {
clearInterval(this.healthCheckTimer);
this.healthCheckTimer = undefined;
}
// Clear batch timer
if (this.batchTimer) {
clearTimeout(this.batchTimer);
this.batchTimer = undefined;
}
// Process any pending batch
if (this.pendingChanges.length > 0) {
this.processBatch();
}
// Clear debounce timers
for (const timer of this.debounceTimers.values()) {
clearTimeout(timer);
}
this.debounceTimers.clear();
// Close all watchers
const closePromises = Array.from(this.watchers.entries()).map(([path, watcher]) => new Promise((resolve) => {
try {
watcher.close();
resolve();
}
catch (error) {
console.warn(`Failed to close watcher for ${path}`);
resolve();
}
}));
await Promise.allSettled(closePromises);
this.watchers.clear();
this.fileHashes.clear();
this.health.activeConnections = 0;
this.isWatching = false;
// Graceful shutdown of child process
if (this.childProcess && this.config.gracefulShutdown) {
await this.gracefulShutdown();
}
this.emit("watcher:stopped", {
uptime: Date.now() - this.startTime,
stats: this.restartStats,
});
}
catch (error) {
this.logError("Stop watcher failed", error.message);
}
}
/**
* Get restart statistics
*/
getRestartStats() {
return { ...this.restartStats };
}
/**
* Get watcher status
*/
getStatus() {
return {
isWatching: this.isWatching,
watchedPaths: Array.from(this.watchers.keys()),
activeWatchers: this.watchers.size,
config: this.config,
stats: this.getRestartStats(),
};
}
/**
* Update configuration
*/
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
this.emit("config:updated", this.config);
}
/**
* Reset restart statistics
*/
resetStats() {
this.restartStats = {
totalRestarts: 0,
lastRestart: null,
averageRestartTime: 0,
restartHistory: [],
successfulRestarts: 0,
failedRestarts: 0,
fastestRestart: Infinity,
slowestRestart: 0,
};
this.emit("stats:reset");
}
}
exports.UltraFastFileWatcher = UltraFastFileWatcher;
exports.default = UltraFastFileWatcher;
//# sourceMappingURL=FileWatcher.js.map