fancive_obsidian-parallel-r.../src/cache-manager.ts
wujunchen 89d28d1113 fix: address all required issues from Obsidian plugin review bot
- Remove `parallel-reader-` prefix from command IDs (Obsidian auto-namespaces)
- Remove default hotkeys to avoid conflicts with user keybindings
- Replace `as TFile` casts with `instanceof TFile` checks
- Make onunload sync, remove detachLeavesOfType (preserves leaf positions)
- Use `vault.configDir` instead of hardcoded `.obsidian`
- Replace `window.confirm` with Obsidian Modal dialog
- Fix unhandled promises with `void` or `await`
- Remove unnecessary `async` from methods without `await`
- Use `new Setting().setHeading()` instead of raw HTML headings
- Fix promise-returning callbacks in event listeners
- Fix error type narrowing with `instanceof Error`
- Apply sentence case to UI text labels

Change-Id: I6e850d8a13b458d5cf1ad91227bf1772636b56b9
2026-04-26 15:57:16 +08:00

160 lines
4.4 KiB
TypeScript

'use strict';
import type { DataAdapter } from 'obsidian';
import { serializeCacheFile, touchCacheEntry } from './cache';
import {
CACHE_SCHEMA_VERSION,
DEFAULT_MAX_CACHE_ENTRIES,
generationFingerprint,
hashContent,
pruneCacheEntries,
} from './settings';
import type { CacheEntry, PluginSettings, RawCard, ResolvedCard } from './types';
export class CacheManager {
cache: Record<string, CacheEntry> = {};
private _timer: ReturnType<typeof setTimeout> | null = null;
private _dirty = false;
constructor(
private readonly adapter: DataAdapter,
private readonly configDir: string,
private readonly pluginId: string,
private readonly getSettings: () => PluginSettings,
) {}
filePath(): string {
return `${this.configDir}/plugins/${this.pluginId}/cache.json`;
}
async ensureDir(): Promise<void> {
const dir = `${this.configDir}/plugins/${this.pluginId}`;
try {
if (typeof this.adapter.exists === 'function' && (await this.adapter.exists(dir))) return;
await this.adapter.mkdir(dir);
} catch (_) {
/* ignore race */
}
}
async readFile(): Promise<Record<string, CacheEntry>> {
try {
const raw = await this.adapter.read(this.filePath());
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object' && parsed.entries && typeof parsed.entries === 'object')
return parsed.entries;
} catch (e: unknown) {
const message = e instanceof Error ? e.message : String(e);
if (!/not found|does not exist|ENOENT/i.test(message))
console.warn('[parallel-reader] failed to read cache.json', e);
}
return {};
}
async writeFile(): Promise<void> {
await this.ensureDir();
await this.adapter.write(this.filePath(), serializeCacheFile(this.cache));
}
async load(): Promise<void> {
this.cache = await this.readFile();
const pruned = this.prune();
if (pruned.length > 0) await this.writeFile();
}
prune(): string[] {
const settings = this.getSettings();
return pruneCacheEntries(this.cache, settings?.maxCacheEntries || DEFAULT_MAX_CACHE_ENTRIES);
}
async pruneIfNeeded(): Promise<string[]> {
const removed = this.prune();
if (removed.length > 0) await this.save();
return removed;
}
async save(): Promise<void> {
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
}
this.prune();
await this.writeFile();
this._dirty = false;
}
scheduleSave(delayMs = 5000): void {
this._dirty = true;
if (this._timer) return;
this._timer = setTimeout(() => {
this._timer = null;
if (!this._dirty) return;
this.save().catch((e) => console.error('[parallel-reader] failed to save cache', e));
}, delayMs);
}
async flush(): Promise<void> {
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
}
if (!this._dirty) return;
await this.save();
}
get(filePath: string): CacheEntry | null {
return this.cache[filePath] || null;
}
async touch(filePath: string): Promise<CacheEntry | null> {
const entry = touchCacheEntry(this.cache[filePath] || null);
if (!entry) return null;
this.cache[filePath] = entry;
this.scheduleSave();
return entry;
}
async put(filePath: string, content: string, cards: RawCard[], settings: PluginSettings): Promise<void> {
const now = new Date().toISOString();
this.cache[filePath] = {
schemaVersion: CACHE_SCHEMA_VERSION,
contentHash: hashContent(content),
settingsHash: generationFingerprint(settings || this.getSettings()),
cards,
generatedAt: now,
lastAccessedAt: now,
};
await this.save();
}
async replaceCards(filePath: string, cards: ResolvedCard[]): Promise<boolean> {
const entry = this.cache[filePath];
if (!entry) return false;
const now = new Date().toISOString();
this.cache[filePath] = {
...entry,
cards: (cards || []).map((card: ResolvedCard) => ({
title: card.title,
anchor: card.anchor,
gist: card.gist,
bullets: card.bullets || [],
})),
updatedAt: now,
lastAccessedAt: now,
};
await this.save();
return true;
}
async delete(filePath: string): Promise<void> {
if (this.cache[filePath]) {
delete this.cache[filePath];
await this.save();
}
}
async clear(): Promise<void> {
this.cache = {};
await this.save();
}
}