logancyang_obsidian-copilot/src/cache/fileCache.ts
Zero Liu 6ca2dc01ea
chore(eslint): enable no-explicit-any; fix ~395 violations (#2452)
* chore(eslint): enable no-explicit-any; fix ~395 violations

Switches @typescript-eslint/no-explicit-any from "off" to "error" and
replaces explicit `any` with proper types or `unknown` + narrowing
across ~100 source files and 15 test files. Eleven eslint-disable
comments remain: one for a BaseLanguageModel prototype patch and ten
for Orama<any> API surfaces where typed alternatives poison Orama's
internal inference to `never`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(scripts): cast chainContext to ChainManager in printPromptDebugEntry

The earlier `as any` → `as unknown` rewrite left buildAgentPromptDebugReport's
chainManager argument typed as `unknown`, which CI's `tsc -noEmit` (without
`--skipLibCheck`) flagged. Restore the cast through the real ChainManager type.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(eslint): fix remaining no-explicit-any and unnecessary-type-assertion errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(eslint): drop redundant no-explicit-any rule

Already set to "error" by typescript-eslint/recommendedTypeChecked via
obsidianmd's recommended config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 02:08:45 -07:00

164 lines
5.2 KiB
TypeScript

import { logError, logInfo } from "@/logger";
import { md5 } from "@/utils/hash";
import { TFile } from "obsidian";
export interface FileCacheEntry<T> {
content: T;
timestamp: number;
}
export class FileCache<T> {
private static instance: FileCache<unknown>;
private cacheDir: string;
private memoryCache: Map<string, FileCacheEntry<T>> = new Map();
private constructor(cacheDir: string) {
this.cacheDir = cacheDir;
}
static getInstance<T>(cacheDir: string = ".copilot/file-content-cache"): FileCache<T> {
if (!FileCache.instance) {
FileCache.instance = new FileCache<T>(cacheDir);
}
return FileCache.instance as FileCache<T>;
}
private async ensureCacheDir() {
if (!(await app.vault.adapter.exists(this.cacheDir))) {
logInfo("Creating file cache directory:", this.cacheDir);
await app.vault.adapter.mkdir(this.cacheDir);
}
}
getCacheKey(file: TFile, additionalContext?: string): string {
// Use file path, size and mtime for a unique but efficient cache key
const metadata = `${file.path}:${file.stat.size}:${file.stat.mtime}${additionalContext ? `:${additionalContext}` : ""}`;
return md5(metadata);
}
private getCachePath(cacheKey: string): string {
return `${this.cacheDir}/${cacheKey}.md`;
}
async get(cacheKey: string): Promise<T | null> {
try {
// Check memory cache first
const memoryResult = this.memoryCache.get(cacheKey);
if (memoryResult) {
logInfo("Memory cache hit for file:", cacheKey);
return memoryResult.content;
}
const cachePath = this.getCachePath(cacheKey);
if (await app.vault.adapter.exists(cachePath)) {
logInfo("File cache hit:", cacheKey);
const cacheContent = await app.vault.adapter.read(cachePath);
// .md files contain either plain string content or JSON-serialized content
// The safest approach is to go back to a simpler method that doesn't try to embed metadata in the content itself.
// Since preserving timestamps in file-based cache is not critical (memory cache handles active sessions)
let parsedContent: T;
// Try to parse as JSON first (for non-string types that were serialized)
const trimmedContent = cacheContent.trim();
if (
(trimmedContent.startsWith("{") && trimmedContent.endsWith("}")) ||
(trimmedContent.startsWith("[") && trimmedContent.endsWith("]"))
) {
try {
parsedContent = JSON.parse(cacheContent);
} catch {
// JSON parsing failed, treat as string content
parsedContent = cacheContent as T;
}
} else {
// Plain text content (primary case for markdown)
parsedContent = cacheContent as T;
}
// Create cache entry for memory storage (file-based cache doesn't preserve timestamps)
const cacheEntry: FileCacheEntry<T> = {
content: parsedContent,
timestamp: Date.now(),
};
// Store in memory cache
this.memoryCache.set(cacheKey, cacheEntry);
return cacheEntry.content;
}
logInfo("Cache miss for file:", cacheKey);
return null;
} catch (error) {
logError("Error reading from file cache:", error);
return null;
}
}
async set(cacheKey: string, content: T): Promise<void> {
try {
await this.ensureCacheDir();
const cachePath = this.getCachePath(cacheKey);
const timestamp = Date.now();
const cacheEntry: FileCacheEntry<T> = {
content,
timestamp,
};
// Store in memory cache
this.memoryCache.set(cacheKey, cacheEntry);
// Serialize content properly for file storage
let serializedContent: string;
if (typeof content === "string") {
// If content is already a string, use it directly
serializedContent = content;
} else {
// For non-string content, serialize as JSON
serializedContent = JSON.stringify(content, null, 2);
}
await app.vault.adapter.write(cachePath, serializedContent);
logInfo("Cached file content:", cacheKey);
} catch (error) {
logError("Error writing to file cache:", error);
}
}
async remove(cacheKey: string): Promise<void> {
try {
// Remove from memory cache
this.memoryCache.delete(cacheKey);
// Remove from file cache (markdown format)
const cachePath = this.getCachePath(cacheKey);
if (await app.vault.adapter.exists(cachePath)) {
await app.vault.adapter.remove(cachePath);
logInfo("Removed file from cache:", cacheKey);
}
} catch (error) {
logError("Error removing file from cache:", error);
}
}
async clear(): Promise<void> {
try {
// Clear memory cache
this.memoryCache.clear();
// Clear file cache
if (await app.vault.adapter.exists(this.cacheDir)) {
const files = await app.vault.adapter.list(this.cacheDir);
logInfo("Clearing file cache, removing files:", files.files.length);
for (const file of files.files) {
await app.vault.adapter.remove(file);
}
}
} catch (error) {
logError("Error clearing file cache:", error);
}
}
}