Skip to main content

Overview

The store manages all application state using Redux Toolkit and Redux-Observable. State is organized into slices, each responsible for a specific domain (playlists, jobs, levels, etc.). The store is configured with epic middleware for handling asynchronous operations.

Store configuration

createStore

Creates the Redux store with epic middleware and dependencies.
dependencies
Dependencies
required
Service implementations (loader, parser, decryptor, fs)
preloadedState
Partial<RootState>
Optional initial state for hydration
Returns: Store - Configured Redux store
import { configureStore, Middleware } from "@reduxjs/toolkit";
import { createEpicMiddleware } from "redux-observable";
import { createRootEpic } from "../controllers/root-epic";
import { Dependencies } from "../services";
import { rootReducer, RootState, RootAction } from "./root-reducer";
import logger from "redux-logger";

export function createStore(
  dependencies: Dependencies,
  preloadedState?: Partial<RootState>
) {
  const epicMiddleware = createEpicMiddleware<
    RootAction,
    RootAction,
    RootState
  >({ dependencies });

  const rootEpic = createRootEpic();

  const store = configureStore<RootState, RootAction, Middleware[]>({
    reducer: rootReducer,
    middleware: [logger, epicMiddleware],
    preloadedState,
  }))

  epicMiddleware.run(rootEpic);
  store.dispatch({
    type: "init/start",
  });
  return store;
}

export type Store = ReturnType<typeof createStore>;
The store automatically dispatches an init/start action after creation, which triggers the initialization epic.

Root reducer

The root reducer combines all slice reducers into a single state tree.
import { combineReducers } from "@reduxjs/toolkit";
import {
  levelsSlice,
  playlistsSlice,
  configSlice,
  tabsSlice,
  jobsSlice,
} from "./slices";
import { levelInspectionsSlice } from "./slices/level-inspections-slice";
import { playlistPreferencesSlice } from "./slices/playlist-preferences-slice";
import { subtitlesSlice } from "./slices/subtitles-slice";

export const rootReducer = combineReducers({
  playlists: playlistsSlice.reducer,
  levels: levelsSlice.reducer,
  config: configSlice.reducer,
  tabs: tabsSlice.reducer,
  jobs: jobsSlice.reducer,
  subtitles: subtitlesSlice.reducer,
  levelInspections: levelInspectionsSlice.reducer,
  playlistPreferences: playlistPreferencesSlice.reducer,
});

export type RootState = ReturnType<typeof rootReducer>;

State slices

Jobs slice

Manages download jobs and their status.
export interface IJobsState {
  jobs: Record<string, Job | null>;
  jobsStatus: Record<string, JobStatus | null>;
}
Job status states:
  • queued: Job is waiting to start
  • downloading: Fragments are being downloaded
  • ready: All fragments downloaded, ready to save
  • saving: File is being saved to disk
  • done: Job completed successfully
  • error: Job failed with error

Playlists slice

Manages HLS master playlists and their fetch status.
export interface IPlaylistsState {
  playlistsStatus: Record<string, PlaylistStatus | null>;
  playlists: Record<string, Playlist | null>;
}

Levels slice

Manages quality levels and tracks (video, audio, subtitle) from playlists.
export interface ILevelsState {
  levels: Record<string, Level | null>;
}

Config slice

Manages application configuration and user preferences.
export interface IConfigState {
  concurrency: number;
  saveDialog: boolean;
  fetchAttempts: number;
  preferredAudioLanguage: string | null;
  maxActiveDownloads: number;
}
Configuration options:
concurrency
number
default:"2"
Number of fragments to download simultaneously
saveDialog
boolean
default:"false"
Whether to show save dialog when downloading
fetchAttempts
number
default:"100"
Maximum retry attempts for failed network requests
preferredAudioLanguage
string | null
default:"null"
Preferred audio language code (e.g., ‘en’, ‘es’)
maxActiveDownloads
number
default:"0"
Maximum number of concurrent download jobs (0 = unlimited)

Tabs slice

Manages browser tab information for tracking playlist sources.
export interface ITabsState {
  tabs: Record<number, Tab | null>;
}

Subtitles slice

Manages downloaded subtitle content before saving.
subtitlesSlice.actions = {
  store: (payload: { levelId: string; text: string }) => Action,
  clear: () => Action,
}

Level inspections slice

Manages encryption inspection results for quality levels.
levelInspectionsSlice.actions = {
  inspect: (payload: { levelId: string }) => Action,
  inspectSuccess: (payload: { inspection: Inspection }) => Action,
  inspectFailed: (payload: { levelId: string; message: string }) => Action,
}

Playlist preferences slice

Manages user preferences for specific playlists.
playlistPreferencesSlice.actions = {
  setPreference: (payload: { playlistId: string; preference: Preference }) => Action,
}

Action types

All actions are typed using typesafe-actions:
export type RootAction =
  | EmptyAction<"init/start">
  | EmptyAction<"init/done">
  | ActionType<typeof jobsSlice.actions>
  | ActionType<typeof tabsSlice.actions>
  | ActionType<typeof configSlice.actions>
  | ActionType<typeof levelsSlice.actions>
  | ActionType<typeof playlistsSlice.actions>
  | ActionType<typeof subtitlesSlice.actions>
  | ActionType<typeof levelInspectionsSlice.actions>
  | ActionType<typeof playlistPreferencesSlice.actions>;

Selectors

Access state using selectors for type safety:
// Get all jobs
const jobs = store.getState().jobs.jobs;

// Get specific job
const job = store.getState().jobs.jobs['job-123'];

// Get job status
const status = store.getState().jobs.jobsStatus['job-123'];

// Get all playlists
const playlists = store.getState().playlists.playlists;

// Get levels for a playlist
const levels = Object.values(store.getState().levels.levels)
  .filter(level => level?.playlistID === 'playlist-1');

// Get config
const config = store.getState().config;

Persistence

The store does not automatically persist state. To persist state across sessions:
// Save state
const state = store.getState();
localStorage.setItem('hls-downloader-state', JSON.stringify(state));

// Load state
const savedState = JSON.parse(localStorage.getItem('hls-downloader-state'));
const store = createStore(dependencies, savedState);

Middleware

The store uses these middleware:
  1. redux-logger: Logs all actions and state changes (development)
  2. epicMiddleware: Runs Redux-Observable epics for async operations

TypeScript types

Export types for use in consuming code:
import type { 
  RootState, 
  RootAction, 
  Store 
} from '@hls-downloader/core';

function useJobs(store: Store) {
  const state: RootState = store.getState();
  return state.jobs;
}

Build docs developers (and LLMs) love