Сохранение данных¶
Всё сохранение данных на фронтенде использует localStorage. На стороне фронтенда нет хранилищ IndexedDB, файловой системы или сетевых хранилищ. Rust обрабатывает сохранение на диск для auth-сессий и логов отладки.
Источники: src/persistence/
Карта ключей хранилища¶
| Ключ | Форма | Назначение |
|---|---|---|
neegde.trackCache.v1 |
Record<id, TrackData> |
Полные данные трека для каждого когда-либо виденного трека |
neegde.albumCache.v1 |
Record<id, AlbumData> |
Полные данные альбома для понравившихся альбомов |
neegde.queue.v2 |
{ trackIds: string[], pos: number } |
Текущая очередь воспроизведения и позиция |
neegde.likes.v2 |
{ trackIds, albumIds, likedAt } |
ID понравившихся треков/альбомов + временные метки |
neegde.playlists.v2 |
{ playlists: PlaylistSnapshot[] } |
Плейлисты пользователя (название + trackIds) |
neegde.player.repeatMode |
"off" \| "all" \| "one" |
Режим повтора |
neegde.player.shuffle |
"0" \| "1" |
Перемешивание вкл/выкл |
torrent_file_b64_v1_<topicId> |
строка base64 | Кешированный файл .torrent для каждого topic-а |
trackCache.ts — хранилище TrackData¶
trackCache — авторитетное хранилище на диске для всех TrackData. Все остальные ключи сохранения (likes, playlists, queue) хранят только ID; данные разрешаются через этот cache.
class TrackCache {
private readonly _data: Map<string, TrackData> = reactive(new Map());
put(t: Track | TrackData): void { ... } // идемпотентно, с дебаунсом
putMany(ts: Array<Track | TrackData>): void { ... }
get(id: string): TrackData | undefined { ... }
hydrateTrack(id: string): Track | null { ... } // buildTrack(data)
}
Путь записи: put() сравнивает новые данные с кешированной версией через проверку поверхностного равенства. Если ничего не изменилось, запись пропускается. Сохранения дебаунсятся на 250 мс — серия вызовов registerEntities из результатов поиска вызывает одну запись в localStorage, а не N.
Путь чтения: hydrateTrack(id) читает из in-memory Map (загруженной из localStorage при запуске) и вызывает buildTrack. Используется очередью и лайками для восстановления экземпляров Track при запуске приложения.
Вытеснение: не предусмотрено. JSON трека занимает ~500 Б. Cache ограничен использованием в человеческом масштабе (лайкнутые треки + записи плейлистов + недавняя очередь). Рост cache не является практической проблемой.
Объединение URL обложек: mergePersistedTrackFields(td) в trackCache.ts копирует coverUrl и albumTitle из сохранённой записи во вновь зарегистрированный entity. Это предотвращает исчезновение обогащения каталога (обложки, загруженной в предыдущей сессии), когда пользователь запускает новый поиск и тот же трек снова отправляется с coverUrl: null.
queue.ts — snapshot очереди¶
export interface QueueSnapshot {
trackIds: string[];
pos: number;
}
const QUEUE_STORAGE_KEY = "neegde.queue.v2";
saveQueueSnapshot записывается при каждой мутации очереди. loadQueueSnapshot вызывается при запуске приложения из PlaybackQueue.seedFromSnapshot. Значение pos ограничивается диапазоном [0, trackIds.length - 1] при загрузке.
likes.ts — понравившиеся ID¶
export interface LikesSnapshot {
trackIds: string[];
albumIds: string[];
likedAt: Record<string, number>; // id → эпоха в мс
}
const LIKES_STORAGE_KEY = "neegde.likes.v2";
likedAt хранит временную метку лайка в миллисекундах с начала эпохи. Computed-значения likedTracks и likedAlbums сортируются по этой временной метке (сначала самые свежие).
Store лайков не записывает данные треков/альбомов здесь — только ID. При запуске приложения bootstrap.ts загружает trackCache + albumCache, гидратирует entities, затем загружает LikesSnapshot. Порядок важен: entities должны быть в реестре до запуска computed likedTracks.
Bootstrap-последовательность¶
src/persistence/bootstrap.ts выполняется один раз при запуске приложения:
// 1. Загрузка cache-ов из localStorage
const trackCacheData = loadTrackCache(); // Map<id, TrackData>
const albumCacheData = loadAlbumCache(); // Map<id, AlbumData>
// 2. Гидратация всех кешированных треков/альбомов в реестр entities
registerEntities([...Object.values(trackCacheData), ...Object.values(albumCacheData)]);
// 3. Загрузка snapshot лайков → ID разрешаются через реестр
const likes = loadLikesSnapshot();
setLikesFromSnapshot(likes);
// 4. Загрузка snapshot очереди → ID разрешаются через реестр
const queue = loadQueueSnapshot();
playbackQueue.seedFromSnapshot(queue);
Если данных понравившегося трека нет в trackCache (пользователь очистил cache, или они никогда не сохранялись), hydrateTrack(id) возвращает null, и трек молча пропускается в computed likedTracks. ID по-прежнему остаётся в likedAt, поэтому если трек впоследствии найден через поиск и зарегистрирован, он снова появляется в разделе «Понравившееся».
generalList.ts — debug-реестр¶
general-list.json — это debug-файл на стороне Rust, записывающий каждый TrackData, который когда-либо регистрировало приложение. Это не пользовательская функция — она существует для отладки истории сессий.
Фронтенд вызывает recordGeneralList(trackData, event) в registerEntity. Это вызывает app_debug_push с категорией "general_list" и данными трека в качестве detail. На стороне Rust general_list_write добавляет запись в general-list.json.
Файл может вырасти до больших размеров при интенсивном использовании. factory_reset (Настройки → Сброс данных приложения) удаляет его вместе с остальными файлами данных.