UNPKG

cui-server

Version:

Web UI Agent Platform based on Claude Code

205 lines 7.26 kB
import fs from 'fs'; import path from 'path'; import { createLogger } from './logger.js'; /** * Simple JSON file manager with race condition protection * Uses file locking and atomic writes to prevent data corruption */ export class JsonFileManager { defaultData; logger; filePath; lockPath; writeQueue = []; isWriting = false; constructor(filePath, defaultData) { this.defaultData = defaultData; this.logger = createLogger('JsonFileManager'); this.filePath = filePath; this.lockPath = `${filePath}.lock`; } /** * Read data from JSON file * Returns default data if file doesn't exist */ async read() { try { // Wait for any pending writes to complete await this.waitForWrite(); if (!fs.existsSync(this.filePath)) { this.logger.debug('File does not exist, returning default data', { filePath: this.filePath }); return JSON.parse(JSON.stringify(this.defaultData)); // Deep copy } const content = await fs.promises.readFile(this.filePath, 'utf-8'); const data = JSON.parse(content); // this.logger.debug('Successfully read JSON file', { filePath: this.filePath }); return data; } catch (error) { this.logger.error('Failed to read JSON file', { filePath: this.filePath, error }); // Return default data on error for graceful degradation return JSON.parse(JSON.stringify(this.defaultData)); } } /** * Write data to JSON file atomically * Uses temporary file and rename to prevent corruption */ async write(data) { return new Promise((resolve, reject) => { // Add to write queue this.writeQueue.push(async () => { try { await this.performWrite(data); resolve(); } catch (error) { reject(error); } }); // Process queue if not already processing if (!this.isWriting) { this.processWriteQueue(); } }); } /** * Update data using a callback function * Reads current data, applies update, and writes back */ async update(updateFn) { const currentData = await this.read(); const updatedData = updateFn(currentData); await this.write(updatedData); } /** * Perform atomic write operation */ async performWrite(data) { const tempPath = `${this.filePath}.tmp`; try { // Create lock file to prevent concurrent writes await this.acquireLock(); // Ensure directory exists const dir = path.dirname(this.filePath); if (!fs.existsSync(dir)) { await fs.promises.mkdir(dir, { recursive: true }); this.logger.debug('Created directory', { dir }); } // Write to temporary file const jsonContent = JSON.stringify(data, null, 2); await fs.promises.writeFile(tempPath, jsonContent, 'utf-8'); // Atomic rename (moves temp file to final location) await fs.promises.rename(tempPath, this.filePath); } catch (error) { this.logger.error('Failed to write JSON file', { filePath: this.filePath, error }); // Clean up temp file if it exists try { if (fs.existsSync(tempPath)) { await fs.promises.unlink(tempPath); } } catch (cleanupError) { this.logger.warn('Failed to cleanup temp file', { tempPath, error: cleanupError }); } throw error; } finally { // Release lock await this.releaseLock(); } } /** * Process write queue sequentially */ async processWriteQueue() { if (this.isWriting) return; this.isWriting = true; try { while (this.writeQueue.length > 0) { const writeOperation = this.writeQueue.shift(); if (writeOperation) { await writeOperation(); } } } finally { this.isWriting = false; } } /** * Wait for any pending writes to complete */ async waitForWrite() { while (this.isWriting) { await new Promise(resolve => setTimeout(resolve, 10)); } } /** * Acquire file lock */ async acquireLock() { const maxRetries = 50; // 5 seconds total let retries = 0; while (retries < maxRetries) { try { // Try to create lock file exclusively await fs.promises.writeFile(this.lockPath, process.pid.toString(), { flag: 'wx' }); return; } catch (error) { if (error && typeof error === 'object' && 'code' in error && error.code === 'EEXIST') { // Lock file exists, check if process is still running try { const lockContent = await fs.promises.readFile(this.lockPath, 'utf-8'); const lockPid = parseInt(lockContent.trim()); // Check if process is still running try { process.kill(lockPid, 0); // Signal 0 just checks if process exists // Process exists, wait and retry await new Promise(resolve => setTimeout(resolve, 100)); retries++; continue; } catch (_processError) { // Process doesn't exist, remove stale lock await fs.promises.unlink(this.lockPath); this.logger.debug('Removed stale lock', { lockPath: this.lockPath }); continue; } } catch (_readError) { // Can't read lock file, remove it try { await fs.promises.unlink(this.lockPath); } catch (_unlinkError) { // Ignore unlink errors } continue; } } else { throw error; } } } throw new Error('Failed to acquire lock after maximum retries'); } /** * Release file lock */ async releaseLock() { try { if (fs.existsSync(this.lockPath)) { await fs.promises.unlink(this.lockPath); } } catch (error) { this.logger.warn('Failed to release lock', { lockPath: this.lockPath, error }); } } } //# sourceMappingURL=json-file-manager.js.map