mirror of
https://github.com/fancive/obsidian-parallel-reader.git
synced 2026-07-22 06:53:43 +00:00
refactor: extract CacheManager from main.ts
Move all cache persistence methods and state (_cacheSaveTimer, _cacheDirty) into a new CacheManager class in src/cache-manager.ts. Plugin delegates to it via thin wrappers that preserve the existing method names tested by the test suite. main.ts reduced from 826 to 713 lines. https://claude.ai/code/session_016QvEfqw6YZ3RjwBHrJ4w8S
This commit is contained in:
parent
f86f45fe73
commit
87904811e2
2 changed files with 198 additions and 156 deletions
197
main.ts
197
main.ts
|
|
@ -2,6 +2,7 @@
|
|||
import { MarkdownView, Notice, Plugin, requestUrl, TFile } from 'obsidian';
|
||||
import { findLineForAnchor } from './src/anchor';
|
||||
import { serializeCacheFile, shouldConfirmRegenerate, touchCacheEntry } from './src/cache';
|
||||
import { CacheManager } from './src/cache-manager';
|
||||
import { activeIndexAfterCardDelete, removeCardAt, updateCardAt } from './src/cards';
|
||||
import { resolveCliPath, summarizeViaClaudeCode, summarizeViaCodex } from './src/cli';
|
||||
import {
|
||||
|
|
@ -30,11 +31,9 @@ import { createRafThrottledHandler, visibleTopProbeY } from './src/scroll';
|
|||
import {
|
||||
CACHE_SCHEMA_VERSION,
|
||||
cacheEntryMatches,
|
||||
DEFAULT_MAX_CACHE_ENTRIES,
|
||||
DEFAULT_SETTINGS,
|
||||
generationFingerprint,
|
||||
getApiBaseUrl,
|
||||
hashContent,
|
||||
isApiBackend,
|
||||
modelForApi,
|
||||
normalizeSettings,
|
||||
|
|
@ -106,12 +105,14 @@ function cancellationNoticeKey(settings: PluginSettings | null, job: GenerationJ
|
|||
|
||||
class ParallelReaderPlugin extends Plugin {
|
||||
settings!: PluginSettings;
|
||||
cache!: Record<string, CacheEntry>;
|
||||
cacheManager!: CacheManager;
|
||||
jobs!: GenerationJobManager;
|
||||
_scrollDispose: (() => void) | null = null;
|
||||
_settingsSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
_cacheSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
_cacheDirty = false;
|
||||
|
||||
get cache(): Record<string, CacheEntry> {
|
||||
return this.cacheManager.cache;
|
||||
}
|
||||
|
||||
t(key: string, vars?: Record<string, string | number>) {
|
||||
return translate(this.settings || DEFAULT_SETTINGS, key, vars);
|
||||
|
|
@ -120,8 +121,6 @@ class ParallelReaderPlugin extends Plugin {
|
|||
async onload() {
|
||||
await this.loadSettings();
|
||||
this.jobs = new GenerationJobManager();
|
||||
this._cacheSaveTimer = null;
|
||||
this._cacheDirty = false;
|
||||
|
||||
this.addRibbonIcon('book-open', this.t('ribbonOpen'), async () => {
|
||||
const active = this.getActiveView();
|
||||
|
|
@ -171,7 +170,7 @@ class ParallelReaderPlugin extends Plugin {
|
|||
callback: async () => {
|
||||
const active = this.getActiveView();
|
||||
if (!active?.file) return new Notice(this.t('noCurrentNote'));
|
||||
await this.cacheDelete(active.file.path);
|
||||
await this.cacheManager.delete(active.file.path);
|
||||
new Notice(this.t('cacheClearedFile', { name: active.file.basename }));
|
||||
},
|
||||
});
|
||||
|
|
@ -179,8 +178,8 @@ class ParallelReaderPlugin extends Plugin {
|
|||
id: 'parallel-reader-clear-all',
|
||||
name: this.t('cmdClearAll'),
|
||||
callback: async () => {
|
||||
const n = Object.keys(this.cache).length;
|
||||
await this.cacheClear();
|
||||
const n = Object.keys(this.cacheManager.cache).length;
|
||||
await this.cacheManager.clear();
|
||||
new Notice(this.t('cacheClearedAll', { count: n }));
|
||||
},
|
||||
});
|
||||
|
|
@ -228,7 +227,13 @@ class ParallelReaderPlugin extends Plugin {
|
|||
const data = (await this.loadData()) || {};
|
||||
const settingsBlob = data.settings || {};
|
||||
this.settings = normalizeSettings(Object.assign({}, DEFAULT_SETTINGS, settingsBlob));
|
||||
await this.loadCache();
|
||||
this.cacheManager = new CacheManager(
|
||||
this.app.vault.adapter,
|
||||
this.app.vault.configDir || '.obsidian',
|
||||
this.manifest?.id || 'parallel-reader',
|
||||
() => this.settings,
|
||||
);
|
||||
await this.cacheManager.load();
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
|
|
@ -254,139 +259,25 @@ class ParallelReaderPlugin extends Plugin {
|
|||
await this.saveSettings();
|
||||
}
|
||||
|
||||
/* ---------- Cache persistence ---------- */
|
||||
|
||||
async saveCache() {
|
||||
if (this._cacheSaveTimer) {
|
||||
clearTimeout(this._cacheSaveTimer);
|
||||
this._cacheSaveTimer = null;
|
||||
}
|
||||
this.pruneCache();
|
||||
await this.writeCacheFile();
|
||||
this._cacheDirty = false;
|
||||
}
|
||||
|
||||
scheduleCacheSave(delayMs = 5000) {
|
||||
this._cacheDirty = true;
|
||||
if (this._cacheSaveTimer) return;
|
||||
this._cacheSaveTimer = setTimeout(() => {
|
||||
this._cacheSaveTimer = null;
|
||||
if (!this._cacheDirty) return;
|
||||
this.saveCache().catch((e) => console.error('[parallel-reader] failed to save cache', e));
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
async flushCacheSave() {
|
||||
if (this._cacheSaveTimer) {
|
||||
clearTimeout(this._cacheSaveTimer);
|
||||
this._cacheSaveTimer = null;
|
||||
}
|
||||
if (!this._cacheDirty) return;
|
||||
await this.saveCache();
|
||||
}
|
||||
|
||||
cacheFilePath() {
|
||||
const configDir = this.app.vault.configDir || '.obsidian';
|
||||
const pluginId = this.manifest?.id || 'parallel-reader';
|
||||
return `${configDir}/plugins/${pluginId}/cache.json`;
|
||||
}
|
||||
|
||||
async ensurePluginDataDir() {
|
||||
const adapter = this.app.vault.adapter;
|
||||
const configDir = this.app.vault.configDir || '.obsidian';
|
||||
const pluginId = this.manifest?.id || 'parallel-reader';
|
||||
const dir = `${configDir}/plugins/${pluginId}`;
|
||||
try {
|
||||
if (typeof adapter.exists === 'function' && (await adapter.exists(dir))) return;
|
||||
await adapter.mkdir(dir);
|
||||
} catch (_) {
|
||||
/* ignore race */
|
||||
}
|
||||
}
|
||||
|
||||
async readCacheFile() {
|
||||
const adapter = this.app.vault.adapter;
|
||||
try {
|
||||
const raw = await adapter.read(this.cacheFilePath());
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object' && parsed.entries && typeof parsed.entries === 'object')
|
||||
return parsed.entries;
|
||||
} catch (e: unknown) {
|
||||
const message = String((e as Error)?.message || e || '');
|
||||
if (!/not found|does not exist|ENOENT/i.test(message))
|
||||
console.warn('[parallel-reader] failed to read cache.json', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async writeCacheFile() {
|
||||
await this.ensurePluginDataDir();
|
||||
await this.app.vault.adapter.write(this.cacheFilePath(), serializeCacheFile(this.cache));
|
||||
}
|
||||
|
||||
async loadCache() {
|
||||
this.cache = await this.readCacheFile();
|
||||
const pruned = this.pruneCache();
|
||||
if (pruned.length > 0) await this.writeCacheFile();
|
||||
}
|
||||
|
||||
pruneCache() {
|
||||
return pruneCacheEntries(this.cache, this.settings?.maxCacheEntries || DEFAULT_MAX_CACHE_ENTRIES);
|
||||
}
|
||||
async pruneCacheIfNeeded() {
|
||||
const removed = this.pruneCache();
|
||||
if (removed.length > 0) await this.saveCache();
|
||||
return removed;
|
||||
}
|
||||
cacheGet(filePath: string) {
|
||||
return this.cache[filePath] || null;
|
||||
}
|
||||
/* ---------- Cache delegation ---------- */
|
||||
|
||||
async cacheTouch(filePath: string) {
|
||||
const entry = touchCacheEntry(this.cache[filePath] || null);
|
||||
if (!entry) return null;
|
||||
this.scheduleCacheSave();
|
||||
return entry;
|
||||
return this.cacheManager.touch(filePath);
|
||||
}
|
||||
|
||||
async cachePut(filePath: string, content: string, cards: RawCard[], settings: PluginSettings) {
|
||||
const now = new Date().toISOString();
|
||||
this.cache[filePath] = {
|
||||
schemaVersion: CACHE_SCHEMA_VERSION,
|
||||
contentHash: hashContent(content),
|
||||
settingsHash: generationFingerprint(settings || this.settings),
|
||||
cards,
|
||||
generatedAt: now,
|
||||
lastAccessedAt: now,
|
||||
};
|
||||
await this.saveCache();
|
||||
scheduleCacheSave(delayMs = 5000) {
|
||||
this.cacheManager.scheduleSave(delayMs);
|
||||
}
|
||||
async flushCacheSave() {
|
||||
await this.cacheManager.flush();
|
||||
}
|
||||
|
||||
async cacheReplaceCards(filePath: string, cards: ResolvedCard[]) {
|
||||
const entry = this.cache[filePath];
|
||||
if (!entry) return false;
|
||||
const now = new Date().toISOString();
|
||||
entry.cards = (cards || []).map((card: ResolvedCard) => ({
|
||||
title: card.title,
|
||||
anchor: card.anchor,
|
||||
gist: card.gist,
|
||||
bullets: card.bullets || [],
|
||||
}));
|
||||
entry.updatedAt = now;
|
||||
entry.lastAccessedAt = now;
|
||||
await this.saveCache();
|
||||
return true;
|
||||
}
|
||||
|
||||
async cacheDelete(filePath: string) {
|
||||
if (this.cache[filePath]) {
|
||||
delete this.cache[filePath];
|
||||
await this.saveCache();
|
||||
}
|
||||
return this.cacheManager.replaceCards(filePath, cards);
|
||||
}
|
||||
async cacheClear() {
|
||||
this.cache = {};
|
||||
await this.saveCache();
|
||||
return this.cacheManager.clear();
|
||||
}
|
||||
async pruneCacheIfNeeded() {
|
||||
return this.cacheManager.pruneIfNeeded();
|
||||
}
|
||||
|
||||
/* ---------- View management ---------- */
|
||||
|
|
@ -480,13 +371,13 @@ class ParallelReaderPlugin extends Plugin {
|
|||
.setIcon('refresh-cw')
|
||||
.onClick(() => this.runForFile(file, true)),
|
||||
);
|
||||
if (this.cacheGet(file.path)) {
|
||||
if (this.cacheManager.get(file.path)) {
|
||||
menu.addItem((it: any) =>
|
||||
it
|
||||
.setTitle(this.t('fileMenuClear'))
|
||||
.setIcon('trash')
|
||||
.onClick(async () => {
|
||||
await this.cacheDelete(file.path);
|
||||
await this.cacheManager.delete(file.path);
|
||||
new Notice(this.t('cacheClearedFile', { name: file.basename }));
|
||||
}),
|
||||
);
|
||||
|
|
@ -499,19 +390,16 @@ class ParallelReaderPlugin extends Plugin {
|
|||
const isMarkdown = file.path.endsWith('.md');
|
||||
if (!wasMarkdown && !isMarkdown) return;
|
||||
if (wasMarkdown && !isMarkdown) {
|
||||
if (this.cache[oldPath]) {
|
||||
delete this.cache[oldPath];
|
||||
await this.saveCache();
|
||||
}
|
||||
await this.cacheManager.delete(oldPath);
|
||||
const view = this.getParallelView();
|
||||
if (view && view.sourceFile?.path === oldPath) view.renderEmpty();
|
||||
return;
|
||||
}
|
||||
if (!wasMarkdown) return;
|
||||
if (this.cache[oldPath]) {
|
||||
this.cache[file.path] = this.cache[oldPath];
|
||||
delete this.cache[oldPath];
|
||||
await this.saveCache();
|
||||
if (this.cacheManager.cache[oldPath]) {
|
||||
this.cacheManager.cache[file.path] = this.cacheManager.cache[oldPath];
|
||||
delete this.cacheManager.cache[oldPath];
|
||||
await this.cacheManager.save();
|
||||
}
|
||||
const view = this.getParallelView();
|
||||
if (view?.sourceFile && (view.sourceFile.path === oldPath || view.sourceFile.path === file.path)) {
|
||||
|
|
@ -522,10 +410,7 @@ class ParallelReaderPlugin extends Plugin {
|
|||
|
||||
async handleFileDelete(file: TFile) {
|
||||
if (!(file instanceof TFile)) return;
|
||||
if (this.cache[file.path]) {
|
||||
delete this.cache[file.path];
|
||||
await this.saveCache();
|
||||
}
|
||||
await this.cacheManager.delete(file.path);
|
||||
const view = this.getParallelView();
|
||||
if (view?.sourceFile?.path === file.path) view.renderEmpty();
|
||||
}
|
||||
|
|
@ -579,7 +464,7 @@ class ParallelReaderPlugin extends Plugin {
|
|||
new Notice(this.t('alreadyGenerating'));
|
||||
return;
|
||||
}
|
||||
if (shouldConfirmRegenerate(this.cacheGet(file.path), force) && !this.confirmRegenerateEditedCards()) {
|
||||
if (shouldConfirmRegenerate(this.cacheManager.get(file.path), force) && !this.confirmRegenerateEditedCards()) {
|
||||
new Notice(this.t('regenerateCancelled'));
|
||||
return;
|
||||
}
|
||||
|
|
@ -600,8 +485,8 @@ class ParallelReaderPlugin extends Plugin {
|
|||
|
||||
job.setPhase('cache-check');
|
||||
if (!force) {
|
||||
const entry = this.cacheGet(file.path);
|
||||
if (cacheEntryMatches(entry, content, this.settings)) {
|
||||
const entry = this.cacheManager.get(file.path);
|
||||
if (entry && cacheEntryMatches(entry, content, this.settings)) {
|
||||
await this.cacheTouch(file.path);
|
||||
if (this.activeFileStillMatches(file))
|
||||
await view.loadFor(file, this.resolveCardAnchors(content, entry.cards), false);
|
||||
|
|
@ -630,7 +515,7 @@ class ParallelReaderPlugin extends Plugin {
|
|||
}
|
||||
const rawCards = sections.map((s) => ({ title: s.title, anchor: s.anchor, gist: s.gist, bullets: s.bullets }));
|
||||
job.setPhase('saving');
|
||||
await this.cachePut(file.path, content, rawCards, this.settings);
|
||||
await this.cacheManager.put(file.path, content, rawCards, this.settings);
|
||||
job.throwIfCancelled();
|
||||
if (this.viewIsShowingFile(view, file)) await view.loadFor(file, sections, false);
|
||||
const unanchored = sections.filter((s) => s.startLine < 0).length;
|
||||
|
|
@ -684,7 +569,7 @@ class ParallelReaderPlugin extends Plugin {
|
|||
if (leaves.length === 0) return;
|
||||
const view = leaves[0].view as ParallelReaderView;
|
||||
if (!view || !this.activeFileStillMatches(file)) return;
|
||||
const entry = this.cacheGet(file.path);
|
||||
const entry = this.cacheManager.get(file.path);
|
||||
if (!entry) {
|
||||
await view.loadFor(file, [], false);
|
||||
view.renderEmptyWithHint(file);
|
||||
|
|
|
|||
157
src/cache-manager.ts
Normal file
157
src/cache-manager.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
'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 = String((e as Error)?.message || 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();
|
||||
entry.cards = (cards || []).map((card: ResolvedCard) => ({
|
||||
title: card.title,
|
||||
anchor: card.anchor,
|
||||
gist: card.gist,
|
||||
bullets: card.bullets || [],
|
||||
}));
|
||||
entry.updatedAt = now;
|
||||
entry.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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue