ryOS ryOS / Docs
GitHub Launch

State Management

ryOS uses Zustand for state management with 40 stores following consistent patterns, combined with localStorage and IndexedDB for persistence. Cloud sync (useCloudSyncStore + useAutoCloudSync + the v2 engine in src/sync/) extends persistence to a remote backend: store state is decomposed into per-key documents, deltas travel as journal ops through /api/sync/v2/*, and small remote changes apply in real time straight from Pusher events (see docs/proposals/cloud-sync-v2.md). 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 --> IDB CHAT --> IDB IPOD --> IDB 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:files (IndexedDB)Virtual filesystem metadata
useThemeStoreryos:theme (manual)OS theme selection + legacy Windows CSS loading
useDisplaySettingsStoreryos:display-settingsWallpaper, shaders, screen saver
useDockStoreryos:dockDock 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, per-category status, deletion markers
useUndoRedoStoreNone (ephemeral)Per-instance undo/redo handler registry for foreground dispatch

App-Specific Stores

StorePersistence KeyPurpose
useChatsStoreryos:chats (IndexedDB)UI/auth metadata in persisted_state; AI messages and room caches in dedicated entity stores
useIpodStoreryos:ipod (IndexedDB)Music library, playback state, lyrics
useVideoStoreryos:videosVideo library, playback
useKaraokeStoreryos:karaokeKaraoke session state
useTerminalStoreryos:terminalCommand history, vim state
useSynthStoreryos:synthSynthesizer presets
useSoundboardStoreryos:soundboard (IndexedDB)Board metadata in persisted_state; recordings stored as Blob rows in soundboard_audio
useAppletStoreryos:appletApplet window dimensions
useFinderStoreryos:finderFinder instances, view preferences
useTextEditStoreryos:textedit (IndexedDB)Scalar metadata in persisted_state; open instances and drafts in textedit_instances
useInternetExplorerStoreryos:internet-explorerFavorites, history
usePaintStoreryos:paintTool settings
usePhotoBoothStoreryos:photoboothRecent photos
usePcStoreryos:pcVirtual PC state
useInfinitePcStoreryos:infinite-pcv86 OS-browser presets and runtime state
useStickiesStoreryos:stickies (IndexedDB)Sticky notes and note layout
useCalendarStoreryos:calendar (IndexedDB)Calendar events, todo items, groups
useContactsStoreryos:contacts (IndexedDB)Address book contacts, vCard import/export
useDashboardStoreryos:dashboardDashboard widget layout and configuration
useListenSessionStoreNone (ephemeral)Listen Together session state and Pusher sync
useInfiniteMacStoreryos:infinite-macInfinite Mac runtime preferences
useBooksStoreryos:booksEPUB book library, reading position, last-opened book
useMapsStoreryos:maps:v1Maps search results, selected place, map region
useTvStoreryos:tvTV channels and playback state
useWeatherStoreryos:weatherWeather data and saved location
useTimezoneStoreryos:timezoneWorld clock timezone selection
useMediaLibraryStore(alias of useIpodStore)Shared music-library/lyrics state for iPod, Karaoke, and Winamp
useAirDropStoreNone (ephemeral)AirDrop transfer session and channel subscription
useWeatherSimulationStoreNone (ephemeral)Desktop weather-effect simulation state
useAdminDashboardStoreNone (ephemeral)Admin dashboard UI/query state

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 and quota-sensitive Zustand slices:

// IndexedDB stores
const STORES = {
  DOCUMENTS: "documents",      // Text files
  IMAGES: "images",           // Binary images
  BOOKS: "books",             // EPUB book files
  BOOK_THUMBNAILS: "book_thumbnails", // Generated book cover thumbnails
  TRASH: "trash",             // Deleted file content
  CUSTOM_WALLPAPERS: "custom_wallpapers",
  APPLETS: "applets",         // HTML applet content
  APPLE_MUSIC_LIBRARY: "apple_music_library",
  APPLE_MUSIC_PLAYLISTS: "apple_music_playlists",
  APPLE_MUSIC_PLAYLIST_TRACKS: "apple_music_playlist_tracks",
  SYNC2_STATE: "sync2_state", // Per-user cursor, shadow, and dirty state
  SOUNDBOARD_AUDIO: "soundboard_audio",
  CHATS_AI_MESSAGES: "chats_ai_messages",
  CHATS_ROOM_MESSAGES: "chats_room_messages",
  TEXTEDIT_INSTANCES: "textedit_instances",
  VFS_ITEMS: "vfs_items",
  PERSISTED_STATE: "persisted_state", // Scalar metadata / unsplit slices
};

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

Quota-sensitive snapshots use createIndexedDBPersistStorage(). Entity-heavy stores use createSplitIndexedDBPersistStorage(), which keeps a small metadata record in persisted_state, migrates monolithic snapshots on hydration, and writes changed entities into dedicated object stores. Both adapters debounce and serialize writes and participate in backup flushing. Because IndexedDB hydration is asynchronous, default seeding and sync jobs wait for persist.hasHydrated() (or the shared usePersistHydrated hook).

Cloud Sync v2 keeps its per-user cursor and shadow in the dedicated sync2_state store. This operational metadata migrates from legacy ryos:sync2:state:* localStorage keys and is intentionally excluded from manual backups.

Hybrid Persistence (Files Store)

The useFilesStore demonstrates a sophisticated two-layer architecture:

flowchart TB
    subgraph "Metadata Layer"
        META[IndexedDB persisted_state
libraryState] FS[IndexedDB vfs_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 META -.->|lifecycle state| FS 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);
  document.documentElement.dataset.osTheme = theme;
  ensureLegacyCss(theme);
},

hydrate: () => {
  const saved = localStorage.getItem("ryos:theme");
  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 = "ryos:auth:username-recovery";

// 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"]
        P3[(ryos:app-store)]
    end

    subgraph LargePersist["IndexedDB persisted_state"]
        P2[(ryos:chats)]
        P1[(ryos:ipod)]
        P4[(ryos:textedit)]
    end

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

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

    S1 --> P1
    S3 --> P4
    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->>IDB: Load quota-sensitive persisted state
    IDB-->>R: Return state (or migrate legacy localStorage)
    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