/** * MetaStore - 동기화 메타데이터 저장소 * * API 동기화 토큰, 마지막 수정 시간, 커서 등 * 로더가 증분 업데이트를 수행하는 데 필요한 메타데이터 저장 */ import type { MetaStore } from "./types"; import * as fs from "fs"; import * as path from "path"; /** * MetaStore 옵션 */ export interface MetaStoreOptions { /** 영속화 파일 경로 */ filePath?: string; /** 자동 저장 활성화 */ autoSave?: boolean; } /** * 직렬화된 메타 스토어 형식 */ interface SerializedMetaStore { version: number; collections: Record>; timestamp: string; } const META_VERSION = 1; /** * 콘텐츠 메타 저장소 구현 */ export class ContentMetaStore { private collections: Map> = new Map(); private options: MetaStoreOptions; private dirty: boolean = false; constructor(options: MetaStoreOptions = {}) { this.options = { autoSave: true, ...options, }; } /** * 컬렉션용 MetaStore 인터페이스 생성 */ getStore(collection: string): MetaStore { if (!this.collections.has(collection)) { this.collections.set(collection, new Map()); } const store = this.collections.get(collection)!; return { get: (key: string) => store.get(key), set: (key: string, value: string) => { store.set(key, value); this.dirty = true; this.autoSave(); }, has: (key: string) => store.has(key), delete: (key: string) => { if (store.has(key)) { store.delete(key); this.dirty = true; this.autoSave(); } }, clear: () => { if (store.size > 0) { store.clear(); this.dirty = true; this.autoSave(); } }, entries: () => Array.from(store.entries()), }; } /** * 컬렉션 삭제 */ deleteCollection(collection: string): void { if (this.collections.has(collection)) { this.collections.delete(collection); this.dirty = true; this.autoSave(); } } /** * 자동 저장 */ private autoSave(): void { if (this.options.autoSave && this.options.filePath) { void this.save(); } } /** * 파일에서 로드 */ async load(): Promise { if (!this.options.filePath) return; try { if (!fs.existsSync(this.options.filePath)) { return; } const content = fs.readFileSync(this.options.filePath, "utf-8"); const data: SerializedMetaStore = JSON.parse(content); if (data.version !== META_VERSION) { console.warn( `[MetaStore] Version mismatch (${data.version} vs ${META_VERSION}), starting fresh` ); return; } for (const [collection, entries] of Object.entries(data.collections)) { const store = new Map(); for (const [key, value] of Object.entries(entries)) { store.set(key, value); } this.collections.set(collection, store); } this.dirty = false; } catch (error) { console.warn( `[MetaStore] Failed to load:`, error instanceof Error ? error.message : error ); } } /** * 파일에 저장 */ async save(): Promise { if (!this.options.filePath || !this.dirty) return; try { const dir = path.dirname(this.options.filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } const data: SerializedMetaStore = { version: META_VERSION, collections: {}, timestamp: new Date().toISOString(), }; for (const [collection, store] of this.collections) { data.collections[collection] = Object.fromEntries(store); } fs.writeFileSync(this.options.filePath, JSON.stringify(data, null, 2)); this.dirty = false; } catch (error) { console.error( `[MetaStore] Failed to save:`, error instanceof Error ? error.message : error ); } } /** * 전체 스토어 초기화 */ clear(): void { this.collections.clear(); this.dirty = true; this.autoSave(); } /** * 스토어 통계 */ getStats(): { collections: number; totalKeys: number } { let totalKeys = 0; for (const store of this.collections.values()) { totalKeys += store.size; } return { collections: this.collections.size, totalKeys, }; } } /** * MetaStore 팩토리 */ export function createMetaStore(options?: MetaStoreOptions): ContentMetaStore { return new ContentMetaStore(options); }