// ──── История повторений (IndexedDB) ──── // Каждая колода хранит до 2000 последних записей. const DB_NAME = 'JapLearnHistory'; const DB_VERSION = 1; export interface RepetitionRecord { id?: number; // автоинкремент set_id: number; word_id: number; timestamp: number; // unix ms mode: string; // easy | ok | hard | none score_before: number; score_after: number; } let db: IDBDatabase | null = null; function openDB(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = () => { const database = request.result; // Создаём store для каждой колоды динамически при первой записи, // но для простоты сделаем один store с индексом по set_id if (!database.objectStoreNames.contains('repetitions')) { const store = database.createObjectStore('repetitions', { keyPath: 'id', autoIncrement: true, }); store.createIndex('set_id', 'set_id', { unique: false }); store.createIndex('timestamp', 'timestamp', { unique: false }); } }; request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { reject(new Error('Не удалось открыть IndexedDB')); }; }); } async function getDB(): Promise { if (db) return db; db = await openDB(); return db; } /** * Добавляет запись о повторении в историю. * После добавления удаляет самые старые записи, если превышен лимит (2000 на колоду). */ export async function addRepetition(record: Omit): Promise { const database = await getDB(); return new Promise((resolve, reject) => { const tx = database.transaction('repetitions', 'readwrite'); const store = tx.objectStore('repetitions'); // Добавляем запись const addRequest = store.add(record); addRequest.onsuccess = () => { // После добавления проверяем лимит trimHistory(record.set_id, 2000); resolve(); }; addRequest.onerror = () => { reject(new Error('Ошибка записи в историю')); }; }); } /** * Удаляет самые старые записи для указанной колоды, оставляя не более limit. */ async function trimHistory(setId: number, limit: number): Promise { const database = await getDB(); return new Promise((resolve, reject) => { const tx = database.transaction('repetitions', 'readwrite'); const store = tx.objectStore('repetitions'); const index = store.index('set_id'); const range = IDBKeyRange.only(setId); const countRequest = index.count(range); countRequest.onsuccess = () => { const count = countRequest.result; if (count <= limit) { resolve(); return; } // Получаем все ID для этой колоды, сортируем по timestamp const allRequest = index.getAllKeys(range); allRequest.onsuccess = () => { // Получаем записи с их id и timestamp для сортировки const cursorRequest = index.openCursor(range); const entries: { id: number; timestamp: number }[] = []; cursorRequest.onsuccess = () => { const cursor = cursorRequest.result; if (cursor) { entries.push({ id: cursor.value.id, timestamp: cursor.value.timestamp }); cursor.continue(); } else { // Сортируем по timestamp (старые первые) entries.sort((a, b) => a.timestamp - b.timestamp); // Сколько нужно удалить const toDelete = entries.length - limit; for (let i = 0; i < toDelete; i++) { store.delete(entries[i].id); } resolve(); } }; }; }; countRequest.onerror = () => reject(new Error('Ошибка подсчёта записей')); }); } /** * Получает историю повторений для указанной колоды, отсортированную по времени (новые первые). */ export async function getHistory(setId: number, limit = 2000): Promise { const database = await getDB(); return new Promise((resolve, reject) => { const tx = database.transaction('repetitions', 'readonly'); const store = tx.objectStore('repetitions'); const index = store.index('set_id'); const range = IDBKeyRange.only(setId); const records: RepetitionRecord[] = []; const cursorRequest = index.openCursor(range, 'prev'); // новые первые cursorRequest.onsuccess = () => { const cursor = cursorRequest.result; if (cursor && records.length < limit) { records.push(cursor.value); cursor.continue(); } else { resolve(records); } }; cursorRequest.onerror = () => { reject(new Error('Ошибка чтения истории')); }; }); } /** * Получает список ID колод, для которых есть история. */ export async function getHistorySetIds(): Promise { const database = await getDB(); return new Promise((resolve, reject) => { const tx = database.transaction('repetitions', 'readonly'); const store = tx.objectStore('repetitions'); const index = store.index('set_id'); const setIdSet = new Set(); const cursorRequest = index.openCursor(); cursorRequest.onsuccess = () => { const cursor = cursorRequest.result; if (cursor) { setIdSet.add(cursor.value.set_id); cursor.continue(); } else { resolve(Array.from(setIdSet)); } }; cursorRequest.onerror = () => { reject(new Error('Ошибка чтения ID колод')); }; }); }