Перейти к содержанию

Сохранение данных

Всё сохранение данных на фронтенде использует 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 (Настройки → Сброс данных приложения) удаляет его вместе с остальными файлами данных.