@signalwire/docusaurus-plugin-llms-txt
Version:
Generate Markdown versions of Docusaurus HTML pages and an llms.txt index file
126 lines (125 loc) • 4.29 kB
JavaScript
/**
* Cache I/O operations
* Focused module for cache file reading, writing, and atomic operations
*/
import { randomUUID } from 'crypto';
import path from 'path';
import fs from 'fs-extra';
import { TEMP_FILE_PREFIX, JSON_INDENT } from '../constants';
import { createCacheError, getErrorCause } from '../errors';
/**
* Validate that loaded data matches CacheSchema structure
*/
function validateCacheSchema(data) {
return (typeof data === 'object' &&
data !== null &&
'pluginVersion' in data &&
'configHash' in data &&
'routes' in data &&
typeof data.pluginVersion === 'string' &&
typeof data.configHash === 'string' &&
Array.isArray(data.routes));
}
/**
* Cache file I/O handler
*/
export class CacheIO {
constructor(_cachePath, _logger) {
this._cachePath = _cachePath;
this._logger = _logger;
}
/**
* Load cache from disk, returning empty cache if file doesn't exist
*/
async loadCache() {
try {
const data = await fs.readJson(this._cachePath);
// Validate the loaded data structure
if (validateCacheSchema(data)) {
return data;
}
else {
// Cache schema is invalid - clean up corrupted cache
const message = 'Cache file format is invalid (possibly from an older plugin version). Clearing corrupted cache and regenerating - this is safe and will not affect your site content.';
this._logger.warn(message);
await this.clearCorruptedCache();
return { pluginVersion: '', configHash: '', routes: [] };
}
}
catch (error) {
// Cache file doesn't exist or can't be read
if (error.code === 'ENOENT') {
// File doesn't exist - this is normal for first run
return { pluginVersion: '', configHash: '', routes: [] };
}
else {
// File exists but is corrupted (parse error, permission error, etc.)
const message = `Cache file corrupted (${error.message}). Clearing cache and regenerating - this will rebuild your content from scratch but will not affect your site.`;
this._logger.warn(message);
await this.clearCorruptedCache();
return { pluginVersion: '', configHash: '', routes: [] };
}
}
}
/**
* Save cache to disk atomically
*/
async saveCache(cache) {
try {
await this.writeJsonAtomic(this._cachePath, cache);
}
catch (error) {
const errorCause = getErrorCause(error);
throw createCacheError('Failed to write cache file', {
cachePath: this._cachePath,
cause: errorCause,
});
}
}
/**
* Atomically writes JSON data to filePath
* Uses temporary file + rename for atomic operation
*/
async writeJsonAtomic(filePath, data) {
const dir = path.dirname(filePath);
await fs.ensureDir(dir);
// Create temporary file with UUID to avoid race conditions
const tmp = path.join(dir, `${TEMP_FILE_PREFIX}${path.basename(filePath)}-${randomUUID()}`);
try {
// Write to temporary file first
await fs.writeFile(tmp, JSON.stringify(data, null, JSON_INDENT), 'utf8');
// Atomic rename to final location
await fs.rename(tmp, filePath);
}
catch (error) {
// Clean up temporary file if write failed
try {
await fs.remove(tmp);
}
catch {
// Ignore cleanup errors
}
throw error;
}
}
/**
* Clear corrupted cache file
*/
async clearCorruptedCache() {
try {
await fs.remove(this._cachePath);
}
catch {
// Ignore cleanup errors - cache directory might not exist or be accessible
}
}
/**
* Get cache file information for debugging
*/
getCacheInfo() {
return {
dir: path.dirname(this._cachePath),
path: this._cachePath,
};
}
}