import type { ContentConverter } from "./converter";
import type { XiaomiNoteClient } from "./client";
import { buildNotesListItem } from "./note";
import type {
FolderEntry,
FullPageData,
NoteEntry,
NotesListItem,
SyncFullResponse,
} from "./types";
export interface NotesCacheOptions {
syncInterval?: number;
autoStart?: boolean;
logger?: Pick<Console, "warn" | "error">;
}
export class NotesCache {
private readonly notes = new Map<string, NoteEntry>();
private readonly folders = new Map<string, FolderEntry>();
private syncTag: string | null = null;
private syncTimer: ReturnType<typeof setInterval> | null = null;
private initialized = false;
private lastSync = 0;
constructor(
private readonly client: XiaomiNoteClient,
private readonly converter: ContentConverter,
private readonly options: NotesCacheOptions = {},
) {}
async init(): Promise<void> {
if (this.initialized) {
return;
}
const snapshot = await this.client.fetchFullPage();
this.applySnapshot(snapshot.data);
this.initialized = true;
if (this.options.autoStart !== false) {
this.startSync();
}
}
startSync(): void {
const interval = this.options.syncInterval ?? 30_000;
this.stopSync();
this.syncTimer = setInterval(() => {
this.syncOnce().catch((error) => {
this.logError("增量同步失败", error);
});
}, interval);
}
stopSync(): void {
if (this.syncTimer) {
clearInterval(this.syncTimer);
this.syncTimer = null;
}
}
dispose(): void {
this.stopSync();
}
getNotes(): NoteEntry[] {
return Array.from(this.notes.values()).sort((a, b) => b.modifyDate - a.modifyDate);
}
getFolders(): FolderEntry[] {
return Array.from(this.folders.values());
}
getNote(id: string): NoteEntry | undefined {
return this.notes.get(id);
}
upsertNote(note: NoteEntry): void {
this.notes.set(note.id, note);
}
upsertFolder(folder: FolderEntry): void {
this.folders.set(folder.id, folder);
}
removeNote(id: string): void {
this.notes.delete(id);
}
removeFolder(id: string): void {
this.folders.delete(id);
}
getNotesListItems(): NotesListItem[] {
return this.getNotes().map((note) => buildNotesListItem(note, this.converter));
}
searchNotes(keyword: string): NotesListItem[] {
const query = keyword.trim().toLowerCase();
if (!query) {
return [];
}
return this.getNotesListItems().filter((item) => {
return (
item.title.toLowerCase().includes(query) ||
item.snippet.toLowerCase().includes(query)
);
});
}
async syncOnce(): Promise<void> {
if (!this.syncTag) {
return;
}
let response: SyncFullResponse;
try {
response = await this.client.syncFull(this.syncTag);
} catch (error) {
this.logError("调用增量同步接口失败", error);
throw error;
}
const envelope = response.data.note_view;
const data = envelope.data;
if (data.entries.length === 0 && data.folders.length === 0) {
this.syncTag = data.syncTag;
this.lastSync = Date.now();
return;
}
data.entries.forEach((entry) => {
if (entry.status === "deleted") {
this.notes.delete(entry.id);
} else {
this.notes.set(entry.id, entry);
}
});
data.folders.forEach((folder) => {
if (folder.status === "deleted") {
this.folders.delete(folder.id);
} else {
this.folders.set(folder.id, folder);
}
});
this.syncTag = data.syncTag;
this.lastSync = Date.now();
}
getSyncTag(): string | null {
return this.syncTag;
}
getLastSyncTime(): number {
return this.lastSync;
}
async refresh(): Promise<void> {
const snapshot = await this.client.fetchFullPage();
this.applySnapshot(snapshot.data);
}
private applySnapshot(snapshot: FullPageData): void {
this.notes.clear();
snapshot.entries.forEach((note) => this.notes.set(note.id, note));
this.folders.clear();
snapshot.folders.forEach((folder) => this.folders.set(folder.id, folder));
this.syncTag = snapshot.syncTag;
this.lastSync = Date.now();
}
private logError(message: string, error: unknown): void {
const logger = this.options.logger ?? console;
logger.error?.(`${message}: ${(error as Error).message}`);
}
}