Files
jap_web/src/history.ts
T
2026-07-03 13:28:26 +03:00

197 lines
6.1 KiB
TypeScript

// ──── История повторений (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<IDBDatabase> {
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<IDBDatabase> {
if (db) return db;
db = await openDB();
return db;
}
/**
* Добавляет запись о повторении в историю.
* После добавления удаляет самые старые записи, если превышен лимит (2000 на колоду).
*/
export async function addRepetition(record: Omit<RepetitionRecord, 'id'>): Promise<void> {
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<void> {
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<RepetitionRecord[]> {
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<number[]> {
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<number>();
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 колод'));
};
});
}