ryOS ryOS / Docs
GitHub Launch

State Management

ryOS uses Zustand for state management with 31 stores following consistent patterns, combined with localStorage and IndexedDB for persistence. Cloud sync (useCloudSyncStore + useAutoCloudSync) extends persistence to a remote backend via Pusher for real-time multi-device synchronization across 11 domains (settings, files, songs, videos, stickies, calendar, contacts, custom wallpapers). CPU-intensive search indexing (Spotlight dynamic results) is offloaded to a dedicated Web Worker rather than running on the main thread.

Architecture Overview

flowchart TB
    subgraph "Presentation Layer"
        COMP[React Components]
    end
    
    subgraph "Store Layer"
        subgraph "Core Stores"
            APP[useAppStore
Window Management] FILES[useFilesStore
File System] THEME[useThemeStore
Theming] end subgraph "Feature Stores" CHAT[useChatsStore] IPOD[useIpodStore] DOCK[useDockStore] end subgraph "Settings Stores" AUDIO[useAudioSettingsStore] DISPLAY[useDisplaySettingsStore] end end subgraph "Persistence Layer" LS[(localStorage)] IDB[(IndexedDB)] end subgraph "External" API[REST APIs] REDIS[(Redis Cache)] end COMP --> APP COMP --> FILES COMP --> CHAT COMP --> IPOD APP -.->|cross-store| FILES IPOD -.->|auth| CHAT APP --> LS FILES --> LS FILES --> IDB CHAT --> LS DISPLAY --> IDB IPOD --> API CHAT --> API API --> REDIS

Store Inventory

Core System Stores

StorePersistence KeyPurpose
useAppStoreryos:app-storeInstance-based window management, boot state, AI model selection, recent apps/documents, expose mode
useFilesStoreryos:filesVirtual filesystem metadata
useThemeStoreryos:theme (manual, with os_theme legacy fallback)OS theme selection + legacy Windows CSS loading
useDisplaySettingsStoreryos:display-settingsWallpaper, shaders, screen saver
useDockStoredock-storageDock pinned items, scale, visibility
useAudioSettingsStoreryos:audio-settingsVolume controls, audio feature toggles
useLanguageStoreryos:language + ryos:language-initialized (manual)UI language preference and first-run detection
useSpotlightStoreNone (ephemeral UI state)Spotlight open/query/selection UI state
useCloudSyncStoreryos:cloud-syncCloud sync preferences, domain status, remote metadata
useUndoRedoStoreNone (ephemeral)Per-instance undo/redo handler registry for foreground dispatch

App-Specific Stores

StorePersistence KeyPurpose
useChatsStoreryos:chatsChat rooms, messages, authentication
useIpodStoreryos:ipodMusic library, playback state, lyrics
useVideoStoreryos:videosVideo library, playback
useKaraokeStoreryos:karaokeKaraoke session state
useTerminalStoreryos:terminalCommand history, vim state
useSynthStoreryos:synthSynthesizer presets
useSoundboardStoreryos:soundboardSoundboard slots and recordings
useAppletStoreapplet-storageApplet window dimensions
useFinderStoreryos:finderFinder instances, view preferences
useTextEditStoreryos:texteditRecent documents
useInternetExplorerStoreryos:internet-explorerFavorites, history
usePaintStoreryos:paintTool settings
usePhotoBoothStoreryos:photoboothRecent photos
usePcStoreryos:pcVirtual PC state
useStickiesStorestickies-storageSticky notes and note layout
useCalendarStorecalendar-storageCalendar events, todo items, groups
useContactsStorecontacts-storageAddress book contacts, vCard import/export
useDashboardStoredashboard-storageDashboard widget layout and configuration
useListenSessionStoreNone (ephemeral)Listen Together session state and Pusher sync
useInfiniteMacStoreryos:infinite-macInfinite Mac runtime preferences

Store Architecture Diagram

graph TD
    subgraph Core["Core System"]
        AppStore[useAppStore
Windows & Boot] ThemeStore[useThemeStore
Theme] DisplayStore[useDisplaySettingsStore
Wallpaper & Shaders] AudioStore[useAudioSettingsStore
Volume & TTS] DockStore[useDockStore
Pinned Items] CloudStore[useCloudSyncStore
Cloud Sync] UndoStore[useUndoRedoStore
Undo/Redo] end subgraph Media["Media Apps"] IpodStore[useIpodStore
Music Library] VideoStore[useVideoStore
Video Library] SynthStore[useSynthStore
Synth Presets] end subgraph Social["Communication"] ChatsStore[useChatsStore
Chat & Auth] end subgraph Productivity["Productivity"] FilesStore[useFilesStore
Virtual FS] TerminalStore[useTerminalStore
CLI State] CalendarStore[useCalendarStore
Events & Todos] ContactsStore[useContactsStore
Contacts] end AppStore --> |manages| Media AppStore --> |manages| Social AppStore --> |manages| Productivity ChatsStore -.-> |auth| IpodStore CloudStore -.-> |sync| FilesStore CloudStore -.-> |sync| IpodStore

Store Pattern

All stores follow a consistent Zustand pattern with persist middleware:

import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

interface ExampleState {
  // State
  someValue: string;
  items: Item[];
  
  // Actions
  setSomeValue: (v: string) => void;
  addItem: (item: Item) => void;
  removeItem: (id: string) => void;
  
  // Computed (via get())
  getItemById: (id: string) => Item | undefined;
}

export const useExampleStore = create<ExampleState>()(
  persist(
    (set, get) => ({
      // Initial state
      someValue: "default",
      items: [],
      
      // Actions
      setSomeValue: (v) => set({ someValue: v }),
      
      addItem: (item) => set((state) => ({
        items: [...state.items, item],
      })),
      
      removeItem: (id) => set((state) => ({
        items: state.items.filter((item) => item.id !== id),
      })),
      
      // Computed getter
      getItemById: (id) => get().items.find((item) => item.id === id),
    }),
    {
      name: "ryos:example",           // localStorage key
      version: 1,                      // Migration version
      storage: createJSONStorage(() => localStorage),
      
      // Selective persistence
      partialize: (state) => ({
        someValue: state.someValue,
        items: state.items,
      }),
      
      // Version migrations
      migrate: (persisted, version) => {
        if (version < 1) {
          // Handle migration
        }
        return persisted;
      },
      
      // Post-rehydration hook
      onRehydrateStorage: () => (state, error) => {
        if (state) {
          // Initialize or sync after rehydration
        }
      },
    }
  )
);

Persistence Strategies

localStorage Persistence (via Zustand persist)

Used for settings, preferences, and lightweight metadata:

persist(
  (set) => ({ /* state and actions */ }),
  {
    name: "ryos:audio-settings",
    version: STORE_VERSION,
    partialize: (state) => ({
      masterVolume: state.masterVolume,
      uiVolume: state.uiVolume,
      // Only persist necessary fields
    }),
  }
)

IndexedDB Integration

Used for large binary data (images, documents, applets):

// IndexedDB stores
const STORES = {
  DOCUMENTS: "documents",      // Text files
  IMAGES: "images",           // Binary images
  APPLETS: "applets",         // HTML applet content
  TRASH: "trash",             // Deleted file content
  CUSTOM_WALLPAPERS: "custom_wallpapers",
};

// Database: ryOS (version 7)
interface StoredContent {
  name: string;               // Original filename
  content: string | Blob;     // File content
}

Hybrid Persistence (Files Store)

The useFilesStore demonstrates a sophisticated two-layer architecture:

flowchart TB
    subgraph "Metadata Layer (localStorage)"
        FS[File System Items
path, name, type, uuid, status] end subgraph "Content Layer (IndexedDB)" DOC[documents store] IMG[images store] APP[applets store] end subgraph "Lazy Loading" CACHE[In-memory cache] PENDING[Pending lazy load map] end FS -->|uuid reference| DOC FS -->|uuid reference| IMG FS -->|uuid reference| APP PENDING -->|on-demand fetch| DOC PENDING -->|on-demand fetch| IMG CACHE -.->|cached JSON| FS

Manual localStorage (Theme Store)

Some stores use manual persistence for more control:

setTheme: (theme) => {
  set({ current: theme });
  localStorage.setItem("ryos:theme", theme);   // Current key
  localStorage.removeItem("os_theme");         // Cleanup legacy key
  document.documentElement.dataset.osTheme = theme;
  ensureLegacyCss(theme);
},

hydrate: () => {
  const saved =
    localStorage.getItem("ryos:theme") ??
    localStorage.getItem("os_theme");          // Legacy fallback
  const theme = saved || "macosx";
  set({ current: theme });
  document.documentElement.dataset.osTheme = theme;
  ensureLegacyCss(theme);
},

Migration System

Version-Based Migrations

migrate: (persistedState: unknown, version: number) => {
  const state = persistedState as StoreState;
  
  if (version < 5) {
    // Add status field and UUIDs
    for (const path in state.items) {
      state.items[path] = {
        ...state.items[path],
        status: state.items[path].status || "active",
        uuid: !state.items[path].isDirectory 
          ? state.items[path].uuid || uuidv4() 
          : undefined,
      };
    }
  }
  
  if (version < 6) {
    // Add timestamp fields
    const now = Date.now();
    for (const path in state.items) {
      state.items[path].createdAt = state.items[path].createdAt || now;
      state.items[path].modifiedAt = state.items[path].modifiedAt || now;
    }
  }
  
  return state;
},

Recovery Mechanisms

Username recovery uses a plain-text localStorage key to survive store resets. Auth tokens are not stored in localStorage — they live exclusively in an httpOnly cookie (ryos_auth) set by the server. On page reload the client calls GET /api/auth/session (the cookie is sent automatically via credentials: "include") to verify the session.

const USERNAME_RECOVERY_KEY = "_usr_recovery_key_";

// Recovery on rehydrate
onRehydrateStorage: () => (state, error) => {
  if (state?.username === null) {
    const recovered = getUsernameFromRecovery();
    if (recovered) state.username = recovered;
  }
  // Auth token is restored from httpOnly cookie, not localStorage
  restoreSessionFromCookie(state.username);
}

Cross-Store Communication

Direct Store Access

Stores can access other stores via getState():

// In useIpodStore - accessing ChatsStore for auth
async function saveLyricOffsetToServer(trackId: string, offset: number) {
  const { username, authToken } = useChatsStore.getState();
  
  if (!username) {
    console.log("Skipping save - user not logged in");
    return false;
  }
  
  // Make authenticated API call.
  // Authorization header is sent when an in-memory token is available;
  // otherwise the httpOnly cookie provides auth automatically.
  const headers: Record<string, string> = {};
  if (authToken) {
    headers["Authorization"] = `Bearer ${authToken}`;
    headers["X-Username"] = username;
  }
  await fetch("/api/songs/...", {
    credentials: "include",
    headers,
  });
}

Store Dependency Pattern

// In useAppStore - accessing AppletStore for window sizing
createAppInstance: (appId, initialData, title) => {
  if (appId === "applet-viewer") {
    const path = (initialData as { path?: string })?.path;
    if (path) {
      const saved = useAppletStore.getState().getAppletWindowSize(path);
      if (saved) size = saved;
    }
  }
}

Event-Based Communication

ryOS now uses typed app-event primitives for most cross-feature interactions:

// Typed launch/update/focus events
requestAppLaunch({ appId: "finder", initialPath: "/Documents" });
emitAppUpdate({ appId: "textedit", instanceId, initialData });
requestWindowFocus({ instanceId });

// Typed global UI toggles
toggleSpotlightSearch();
toggleExposeView();
selectExposeWindow({ instanceId });

// Typed document/applet update events
emitDocumentUpdated({ path, content });
emitAppletUpdated({ path, content });

Legacy window.dispatchEvent(new CustomEvent(...)) broadcasts still exist for a few low-level flows (for example instanceStateChange, wallpaperChange, and close-request events), but new app-level integrations should use src/utils/appEventBus.ts.

Cross-Store Dependencies Map

flowchart LR
    subgraph Components["React Components"]
        C1[IpodApp]
        C2[KaraokeApp]
        C3[TextEditApp]
    end

    subgraph Stores["Zustand Stores"]
        S1[useIpodStore]
        S2[useKaraokeStore]
        S3[useTextEditStore]
        S4[useChatsStore]
        S5[useAppStore]
    end

    subgraph Persist["localStorage"]
        P1[(ryos:ipod)]
        P2[(ryos:chats)]
        P3[(ryos:app-store)]
    end

    C1 <--> S1
    C2 <--> S2
    C3 <--> S3

    S2 -.-> |music library| S1
    S3 -.-> |foreground instance| S5
    S1 -.-> |auth credentials| S4

    S1 --> P1
    S4 --> P2
    S5 --> P3

State Flow Patterns

App Instance Lifecycle

stateDiagram-v2
    [*] --> Creating: launchApp()
    
    Creating --> Loading: createAppInstance()
    Loading --> Foreground: markInstanceAsLoaded()
    
    Foreground --> Background: bringInstanceToForeground(other)
    Background --> Foreground: bringInstanceToForeground(this)
    
    Foreground --> Minimized: minimizeInstance()
    Background --> Minimized: minimizeInstance()
    Minimized --> Foreground: restoreInstance()
    
    Foreground --> [*]: closeAppInstance()
    Background --> [*]: closeAppInstance()
    Minimized --> [*]: closeAppInstance()

Library Initialization Flow

sequenceDiagram
    participant R as Rehydrate
    participant S as Store
    participant LS as localStorage
    participant IDB as IndexedDB
    participant API as External API
    
    R->>LS: Load persisted state
    LS-->>R: Return state (or empty)
    R->>S: Set initial state
    
    alt libraryState === "uninitialized"
        S->>API: loadDefaultFiles()
        API-->>S: JSON data
        S->>LS: Persist metadata
        S->>IDB: Save text content
        S->>S: registerFilesForLazyLoad(assets)
    else libraryState === "loaded"
        S->>S: syncRootDirectories()
        S->>S: ensureDefaultDesktopShortcuts()
    end

Spotlight Search Indexing (Worker Offload)

useSpotlightSearch builds snapshots from useFilesStore, useIpodStore, useInternetExplorerStore, useVideoStore, useCalendarStore, and useContactsStore, then posts indexing/query requests to spotlightSearch.worker.ts. Initial indexing is deferred with requestIdleCallback (fallback timeout), which keeps first-render interactions responsive while search data is prepared in the background.

Best Practices

Selective Persistence (partialize)

partialize: (state) => ({
  // Only persist necessary state, exclude:
  // - Transient UI state (isLoading, etc.)
  // - Large computed data
  // - Runtime-only values
  tracks: state.tracks,
  currentSongId: state.currentSongId,
  // NOT: isPlaying, currentLyrics, currentFuriganaMap
}),

Optimized Selectors (useShallow)

import { useShallow } from "zustand/react/shallow";

// Prevents unnecessary re-renders when selecting multiple values
function useAppStoreShallow<T>(
  selector: (state: AppStoreState) => T
): T {
  return useAppStore(useShallow(selector));
}

// Usage
const { instances, foregroundInstanceId } = useAppStoreShallow((s) => ({
  instances: s.instances,
  foregroundInstanceId: s.foregroundInstanceId,
}));

Debounced Server Sync

const lyricOffsetSaveTimers = new Map<string, NodeJS.Timeout>();

function debouncedSaveLyricOffset(trackId: string, offset: number): void {
  const existing = lyricOffsetSaveTimers.get(trackId);
  if (existing) clearTimeout(existing);

  const timer = setTimeout(() => {
    lyricOffsetSaveTimers.delete(trackId);
    saveLyricOffsetToServer(trackId, offset);
  }, 2000); // 2 second debounce

  lyricOffsetSaveTimers.set(trackId, timer);
}

Data Caching with Deduplication

let cachedFileSystemData: FileSystemData | null = null;
let fileSystemDataPromise: Promise<FileSystemData> | null = null;

async function loadDefaultFiles(): Promise<FileSystemData> {
  if (cachedFileSystemData) return cachedFileSystemData;
  if (fileSystemDataPromise) return fileSystemDataPromise;  // Dedup in-flight
  
  fileSystemDataPromise = (async () => {
    const data = await fetch("/data/filesystem.json").then(r => r.json());
    cachedFileSystemData = data;
    return data;
  })();
  
  return fileSystemDataPromise;
}

Immutable Updates

// Array updates without mutation
set((state) => ({
  tracks: state.tracks.map((track, i) =>
    i === trackIndex ? { ...track, lyricOffset: newOffset } : track
  ),
}));

// Object updates without mutation
set((state) => ({
  instances: {
    ...state.instances,
    [instanceId]: { ...state.instances[instanceId], isForeground: true },
  },
}));

Related Documentation