197 lines
6.1 KiB
TypeScript
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 колод'));
|
|
};
|
|
});
|
|
} |