Frontend stores¶
Фронтенд использует паттерн синглтона на уровне модуля — без Pinia, без Vuex. Каждый store — это обычный TypeScript-модуль, экспортирующий реактивные примитивы Vue (ref, shallowRef, computed). Компоненты импортируют только нужные им символы.
Источники: src/stores/
entities.ts — глобальный реестр entities¶
Реестр entities — это единственный источник правды для каждого Track и Album, которые когда-либо видело приложение. Provider-ы отправляют данные; реестр преобразует их в экземпляры классов и уведомляет потребителей.
const _byId = shallowRef<Map<string, Entity>>(new Map());
export const entitiesVersion = shallowRef(0);
shallowRef оборачивает Map. Реактивность Vue не отслеживает мутации Map нативно — поэтому каждая запись вызывает triggerRef(_byId) и инкрементирует entitiesVersion. Потребители, вызывающие getTrack(id) внутри computed, должны также читать entitiesVersion.value, чтобы подписаться на сигнал инвалидации.
Регистрация¶
registerEntity(entity) нормализует входные данные (плоские данные → экземпляр класса через buildTrack / buildAlbum), объединяет сохранённые поля из trackCache и записывает в map. Для треков также вызывает putTrack (запись в trackCache) и recordGeneralList (добавление в debug-реестр general-list.json).
registerEntities(entities[]) — пакетная версия: один вызов bump() для всего набора.
Побеждают более поздние копии. Повторные отправки от provider-а (новый snapshot из той же сессии) заменяют существующие записи. Это означает, что более высокоранговая или объединённая из нескольких источников версия той же entity побеждает при последующих циклах сброса.
Методы доступа¶
getEntity(id) → Entity | null
getTrack(id) → Track | null
getAlbum(id) → Album | null
getTracksOfAlbum(alb) → Track[] // в порядке trackIds
allEntities() → Entity[] // затратное копирование
queue.ts — очередь воспроизведения¶
Очередь хранит только ID треков. Метаданные (название, URL, обложка) разрешаются через реестр entities во время чтения. Это позволяет избежать дублирования данных и означает, что обогащение каталога (например, загрузка обложки после постановки трека в очередь) отображается автоматически.
Класс PlaybackQueue¶
class PlaybackQueue {
readonly ids: Ref<string[]>;
readonly pos: Ref<number>;
readonly repeatMode: Ref<RepeatMode>; // "off" | "all" | "one"
readonly shuffleOn: Ref<boolean>;
readonly nowPlaying: ComputedRef<Track | null>;
readonly next: ComputedRef<Track | null>; // учитывает repeatMode
readonly secondNext: ComputedRef<Track | null>; // используется для warm prefetch
readonly hasPrev: ComputedRef<boolean>;
readonly hasNext: ComputedRef<boolean>;
readonly tracks: ComputedRef<Track[]>; // полная очередь в виде Track[]
readonly suppressAutoplay: Ref<boolean>; // блокирует HTML-автовоспроизведение при восстановлении
}
nowPlaying, next и secondNext читают entitiesVersion.value как зависимость, чтобы пересчитываться при изменении реестра (например, когда приходит обложка для текущего трека).
suppressAutoplay устанавливается во время seedFromSnapshot (восстановление при холодном старте), чтобы аудиоэлемент не начинал воспроизведение автоматически при присвоении src из сохранённой очереди. Сбрасывается, когда пользователь нажимает play (allowAutoplay()).
Повтор и перемешивание¶
repeatMode и shuffleOn сохраняются в localStorage (neegde.player.repeatMode, neegde.player.shuffle). hasPrev и hasNext учитывают repeatMode = "all" — очередь закольцовывается.
Реализация перемешивания запланирована, но ещё не реализована; shuffleOn отслеживается для состояния UI.
Сохранение¶
Каждая мутация вызывает saveQueueSnapshot, который записывает { ids, pos } в localStorage. При запуске приложения seedFromSnapshot заново заполняет очередь и вызывает hydrateTrack для каждого ID (читает из trackCache), чтобы очередь сохранялась между перезапусками без нового поиска.
search.ts — store поиска¶
Store поиска владеет синглтоном SearchEngine и связывает его с реактивным состоянием Vue, которое потребляют компоненты.
export const searchEntities = shallowRef<Entity[]>([]);
export const searchLoadingRt = ref<boolean>(false);
export const searchLoadingSlsk = ref<boolean>(false);
export const searchRtError = ref<string | null>(null);
export const searchSlskError = ref<string | null>(null);
export const searchError = ref<string | null>(null);
export const searchResultsEpoch = ref<number>(0); // для сброса бесконечной прокрутки в Results.vue
export const searchResolving = ref<boolean>(false);
export const searchResolved = shallowRef<unknown>(null); // ResolveResult от resolver-а
export const searchProviderQuery = ref<string>("");
runSearch(query)¶
Главная точка входа:
- Инкрементирует
_epochSeq, чтобы устаревшие ответы resolver-а от предыдущих вызовов игнорировались. - Устанавливает
searchResolving = true. - Вызывает
engine.query(query, enabledProviders), который возвращаетSearchSession. - Наблюдает за
session.resultsчерезwatch(session.results, _applyFromSession). _applyFromSessionвызываетregisterEntities(session.results.value)и копируетsearchEntities,searchLoadingRt,searchLoadingSlskи поля ошибок из сессии.
Когда сессия заменяется новым запросом, _detach() останавливает watcher и очищает ссылку на сессию.
Обработка ошибок¶
После завершения работы обоих provider-ов:
- 0 entities + нет авторизации (не авторизован ни в одном) → searchError = null (без лишнего шума)
- 0 entities + ошибка provider-а → показать сообщение об ошибке
- 0 entities + нет ошибки → "Ничего не найдено."
library.ts — лайки и плейлисты¶
Store библиотеки оборачивает два синглтона на основе классов: LikesCollection и Library.
Лайки¶
export const likes = new LikesCollection(saveLikesSnapshot);
export const likedTrackIds = likes.trackIds; // Ref<Set<string>>
export const likedAlbumIds = likes.albumIds; // Ref<Set<string>>
export const likedAt = likes.likedAt; // Ref<Map<string, timestamp>>
likedTracks и likedAlbums — это computed-методы доступа, сортирующие по временной метке likedAt (сначала самые свежие) и разрешающие ID через реестр entities.
isTrackLiked(id) / isAlbumLiked(id) — поиск в Set за O(1).
Каждая мутация синхронизируется с localStorage через saveLikesSnapshot.
Плейлисты¶
Класс Library владеет упорядоченным списком объектов Playlist. Каждый плейлист хранит ID треков и название. Как и в очереди, записи плейлиста разрешаются через реестр entities во время чтения.
auth.ts — статус подключения¶
Реактивное состояние входа/подключения для обоих provider-ов:
export const rtLoggedIn = ref<boolean>(false);
export const rtUsername = ref<string | null>(null);
export const rtAvatarUrl = ref<string | null>(null);
export const slskConnected = ref<boolean>(false);
export const slskUsername = ref<string | null>(null);
Устанавливаются обработчиками команд авторизации (ответы на вход/выход/восстановление сессии) и читаются компонентами AppAuthPanel, SearchBar и store поиска.
view.ts — состояние навигации¶
Текущее отображаемое представление и история навигации:
Используется App.vue для рендеринга правильного компонента и компонентом NavArrows для навигации вперёд/назад.
Инвариант реактивности¶
Все store-ы используют shallowRef (не ref) для объектов и map-ов. shallowRef не делает внутренности объекта реактивными — только переприсвоение .value запускает watcher-ы. Это намеренно: объекты entity крупные и глубоко вложенные, и глубокая реактивность вызвала бы избыточные перерисовки. Счётчик entitiesVersion — это явный сигнал, управляющий пересчётом.