v0.1.8
Menu
Download
Zustand State Store

Zustand State Store

One Zustand store is the main-window brain: nav, settings, download queue, gallery list, and hero metadata.

useRuforgeStore in ruforgeStore.ts merges slices into a single create() call. createDownloadQueueSlice owns jobs, focus row, enqueue/pause/finish handlers, and legacy hero fields (url, videoInfo, metadataLoading). Gallery state holds entries, galleryLoading, and scan revision counters.

Components subscribe with selectors (useRuforgeStore(s => s.activeTab)) to avoid rerendering the whole tree when unrelated fields change. Player scrub hover, open menus, and transient downloader UI stay in local useState on purpose.

Persistence uses ruforgePersistStorage.ts: flat localStorage keys for settings and output paths so older builds and mini-player readers stay compatible. Download jobs also persist to survive restarts mid-queue. Gallery entries reload from disk via fetchEntries after boot.

Zustand does not cross webviews. Mini player keeps its own playback state and syncs through Tauri events. When debugging playback handoff, check both surfaces.

In the repo

export const useRuforgeStore = create<RuforgeStore>()(
  persist(
    (set, get, store) => ({
      ...createDownloadQueueSlice(set, get, store),
      downloadJobs: initialDownloadQueue.downloadJobs,
      focusedJobId: initialDownloadQueue.focusedJobId,

      settings: DEFAULT_SETTINGS,
      outputDir: pathsInit.outputDir,
      saveToInternal: pathsInit.saveToInternal,
      isSidebarExpanded: pathsInit.isSidebarExpanded,
      storageStats: null,

      activeTab: "downloader",
      settingsTab: "general",
      galleryFilter: "all",
      selectedPlaylist: null,
      isSearchExpanded: false,
      searchValue: "",
      lastExplorerUrl: "https://www.youtube.com",

      notifications: [],

      url: "",
      downloaderDuplicateDialogOpen: false,
      metadataLoading: false,
      downloading: false,
      progress: null,
      videoInfo: null,
      videoInfoUrl: null,
      videoInfoPreferredQuality: null,
      metadataError: null,
      isFocused: false,

      entries: [],
      libraryScanRevision: 0,
    {
      name: "ruforge-main",
      storage: createRuforgePersistStorage(),
      partialize: (s): RuforgePersistedSubset => ({
        settings: s.settings,
        outputDir: s.outputDir,
        saveToInternal: s.saveToInternal,
      }),
      /** Must match `version` returned from `getItem` in `createRuforgePersistStorage` (both 0). */
      version: 0,
      /**
       * Runs once after rehydration. With synchronous `getItem`, hydration finishes during
       * `create()` before React mounts — so tray/autostart `invoke` fire at store init time,
       * not in a post-mount effect. That is intentional (single sync from persisted prefs);
       * Tauri IPC is available here in the normal Vite+Tauri bootstrap order.
       */
    onDownloadJobFinished: (payload) => {
      disarmDownloadJobWatchdog(payload.jobId);
      resetDownloadProgressEtaSmoothing(payload.jobId);
      const starts: { id: string; url: string; resume: boolean }[] = [];
      const skippedIds: string[] = [];
      let finishedUrl: string | undefined;

      set((s) => {
        const finishedJob = s.downloadJobs.find((j) => j.id === payload.jobId);
        finishedUrl =
          payload.url?.trim() || finishedJob?.url?.trim() || undefined;

        let downloadJobs = s.downloadJobs.map((j) =>
          j.id === payload.jobId
            ? {
                ...j,
                status: payload.success ? ("completed" as const) : ("failed" as const),
                error: payload.error ?? null,
                progress: payload.success ? j.progress : j.progress,
                resumeOnStart: false,
              }
            : j,
        );

        if (payload.success) {
          downloadJobs = downloadJobs.filter(
            (j) =>
              j.id !== payload.jobId &&
              !(finishedUrl && youtubeUrlsMatch(j.url, finishedUrl)),
          );
        }

        persistDownloadJobs(downloadJobs);

        const {
          jobs: promotedJobs,
          starts: batchStarts,
          skippedIds: batchSkipped,
        } = promoteEligibleJobs(downloadJobs, s.maxConcurrentDownloads, get);
        downloadJobs = promotedJobs;
        starts.push(...batchStarts);
        skippedIds.push(...batchSkipped);
        persistDownloadJobs(downloadJobs);

        const focus = resolveFocusAfterMutation(downloadJobs, s.focusedJobId);

        return {
          downloadJobs,
          focusedJobId: focus,
          ...syncLegacyDownloaderUi(downloadJobs, focus),
          ...(payload.success ? heroClearPatchForUrl(s, finishedUrl) : {}),
        };

Where it shows up

  • src/store/ruforgeStore.ts main store composition
  • src/store/downloadQueueSlice.ts queue CRUD, finish handler, hero clear
  • ruforgePersistStorage.ts flat key mapping
  • Not persisted: transient player UI, explorer shimmer, modal open flags