gather-ts
Version:
A powerful code analysis and packaging tool designed for creating AI-friendly code representations for javascript and typescript projects.
228 lines • 8.88 kB
JavaScript
"use strict";
// src/core/tokenization/TokenCache.ts
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TokenCache = void 0;
const crypto_1 = __importDefault(require("crypto"));
const errors_1 = require("@/errors");
const services_1 = require("@/types/services");
class TokenCache extends services_1.BaseService {
constructor(projectRoot, deps, options = {}) {
super();
this.deps = deps;
this.cache = {};
this.stats = {
totalEntries: 0,
oldestEntry: null,
newestEntry: null,
totalSize: 0,
hitCount: 0,
missCount: 0,
invalidations: 0,
};
this.projectRoot = projectRoot;
this.maxCacheAge = options.maxCacheAge || 7 * 24 * 60 * 60 * 1000; // 7 days default
this.debug = options.debug || false;
this.cachePath = this.deps.fileSystem.joinPath(projectRoot, ".gather-ts", "token-cache.json");
}
async initialize() {
this.logDebug("Initializing TokenCache");
try {
await super.initialize();
if (!this.deps.fileSystem.exists(this.projectRoot)) {
throw new errors_1.ValidationError("Project root does not exist", {
projectRoot: this.projectRoot,
});
}
await this.initializeCachePath();
await this.loadCache();
}
catch (error) {
throw new errors_1.CacheError(`Failed to initialize TokenCache: ${error instanceof Error ? error.message : String(error)}`, "write");
}
}
async initializeCachePath() {
try {
const cacheDir = this.deps.fileSystem.getDirName(this.cachePath);
if (!this.deps.fileSystem.exists(cacheDir)) {
await this.deps.fileSystem.createDirectory(cacheDir, true);
this.logDebug(`Created cache directory at ${cacheDir}`);
}
}
catch (error) {
throw new errors_1.CacheError(`Failed to initialize cache path: ${error instanceof Error ? error.message : String(error)}`, "initialize");
}
}
cleanup() {
this.logDebug("Cleaning up TokenCache");
this.saveCache();
super.cleanup();
}
logDebug(message) {
if (this.debug) {
this.deps.logger.debug(message);
}
}
handleError(operation, error, key) {
const message = error instanceof Error ? error.message : String(error);
const cacheError = new errors_1.CacheError(`Cache ${operation} failed: ${message}`, operation, key);
this.deps.logger.error(cacheError.message);
throw cacheError;
}
validateCacheEntry(entry) {
if (!entry || typeof entry !== "object") {
return false;
}
const candidate = entry;
return (typeof candidate.hash === "string" &&
typeof candidate.tokens === "number" &&
typeof candidate.lastUpdated === "string" &&
!isNaN(Date.parse(candidate.lastUpdated)));
}
getRelativeCachePath(absolutePath) {
return this.deps.fileSystem.getRelativePath(this.projectRoot, absolutePath);
}
async loadCache() {
try {
if (this.deps.fileSystem.exists(this.cachePath)) {
const content = await this.deps.fileSystem.readFile(this.cachePath);
const parsed = JSON.parse(content);
if (typeof parsed !== "object" || parsed === null) {
throw new errors_1.ValidationError("Invalid cache file structure");
}
const validatedCache = {};
const now = Date.now();
let expiredCount = 0;
let invalidCount = 0;
Object.entries(parsed).forEach(([key, entry]) => {
if (!this.validateCacheEntry(entry)) {
invalidCount++;
this.logDebug(`Invalid cache entry for ${key}, skipping`);
return;
}
const entryAge = now - Date.parse(entry.lastUpdated);
if (entryAge > this.maxCacheAge) {
expiredCount++;
this.stats.invalidations++;
this.logDebug(`Skipping expired cache entry for ${key}`);
return;
}
validatedCache[key] = entry;
});
this.cache = validatedCache;
this.updateStats();
this.logDebug(`Loaded ${Object.keys(this.cache).length} valid cache entries ` +
`(${expiredCount} expired, ${invalidCount} invalid)`);
}
}
catch (error) {
this.handleError("read", error);
}
}
async saveCache() {
try {
await this.deps.fileSystem.writeFile(this.cachePath, JSON.stringify(this.cache, null, 2));
this.logDebug(`Saved ${Object.keys(this.cache).length} cache entries`);
}
catch (error) {
this.handleError("write", error);
}
}
computeHash(content) {
try {
return crypto_1.default.createHash("md5").update(content).digest("hex");
}
catch (error) {
this.handleError("hash", error);
}
}
getCachedTokenCount(filePath, content) {
if (!filePath || typeof filePath !== "string") {
throw new errors_1.ValidationError("Invalid file path provided", { filePath });
}
try {
const relativePath = this.getRelativeCachePath(filePath);
const hash = this.computeHash(content);
const cached = this.cache[relativePath];
if (cached && cached.hash === hash) {
const entryAge = Date.now() - Date.parse(cached.lastUpdated);
if (entryAge > this.maxCacheAge) {
this.logDebug(`Cache entry for ${relativePath} has expired`);
delete this.cache[relativePath];
this.stats.invalidations++;
this.stats.missCount++;
return null;
}
this.stats.hitCount++;
return cached.tokens;
}
this.stats.missCount++;
return null;
}
catch (error) {
this.handleError("read", error, filePath);
}
}
cacheTokenCount(filePath, content, tokens) {
if (!filePath || typeof filePath !== "string") {
throw new errors_1.ValidationError("Invalid file path provided", { filePath });
}
if (typeof tokens !== "number" || isNaN(tokens) || tokens < 0) {
throw new errors_1.ValidationError("Invalid token count provided", {
filePath,
tokens,
});
}
try {
const relativePath = this.getRelativeCachePath(filePath);
const entry = {
hash: this.computeHash(content),
tokens,
lastUpdated: new Date().toISOString(),
};
this.cache[relativePath] = entry;
this.updateStats();
this.saveCache();
this.logDebug(`Cached ${tokens} tokens for ${relativePath}`);
}
catch (error) {
this.handleError("write", error, filePath);
}
}
clear() {
try {
this.cache = {};
this.updateStats();
if (this.deps.fileSystem.exists(this.cachePath)) {
this.deps.fileSystem.deleteFile(this.cachePath);
this.logDebug("Cleared cache file");
}
}
catch (error) {
this.handleError("clear", error);
}
}
updateStats() {
const entries = Object.values(this.cache);
let oldestDate = Date.now();
let newestDate = 0;
entries.forEach((entry) => {
const timestamp = Date.parse(entry.lastUpdated);
oldestDate = Math.min(oldestDate, timestamp);
newestDate = Math.max(newestDate, timestamp);
});
this.stats.totalEntries = entries.length;
this.stats.oldestEntry =
entries.length > 0 ? new Date(oldestDate).toISOString() : null;
this.stats.newestEntry =
entries.length > 0 ? new Date(newestDate).toISOString() : null;
this.stats.totalSize = Buffer.byteLength(JSON.stringify(this.cache));
}
getCacheStats() {
return { ...this.stats };
}
}
exports.TokenCache = TokenCache;
//# sourceMappingURL=TokenCache.js.map