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
| Store | Persistence Key | Purpose |
|---|---|---|
useAppStore | ryos:app-store | Instance-based window management, boot state, AI model selection, recent apps/documents, expose mode |
useFilesStore | ryos:files | Virtual filesystem metadata |
useThemeStore | ryos:theme (manual, with os_theme legacy fallback) | OS theme selection + legacy Windows CSS loading |
useDisplaySettingsStore | ryos:display-settings | Wallpaper, shaders, screen saver |
useDockStore | dock-storage | Dock pinned items, scale, visibility |
useAudioSettingsStore | ryos:audio-settings | Volume controls, audio feature toggles |
useLanguageStore | ryos:language + ryos:language-initialized (manual) | UI language preference and first-run detection |
useSpotlightStore | None (ephemeral UI state) | Spotlight open/query/selection UI state |
useCloudSyncStore | ryos:cloud-sync | Cloud sync preferences, domain status, remote metadata |
useUndoRedoStore | None (ephemeral) | Per-instance undo/redo handler registry for foreground dispatch |
App-Specific Stores
| Store | Persistence Key | Purpose |
|---|---|---|
useChatsStore | ryos:chats | Chat rooms, messages, authentication |
useIpodStore | ryos:ipod | Music library, playback state, lyrics |
useVideoStore | ryos:videos | Video library, playback |
useKaraokeStore | ryos:karaoke | Karaoke session state |
useTerminalStore | ryos:terminal | Command history, vim state |
useSynthStore | ryos:synth | Synthesizer presets |
useSoundboardStore | ryos:soundboard | Soundboard slots and recordings |
useAppletStore | applet-storage | Applet window dimensions |
useFinderStore | ryos:finder | Finder instances, view preferences |
useTextEditStore | ryos:textedit | Recent documents |
useInternetExplorerStore | ryos:internet-explorer | Favorites, history |
usePaintStore | ryos:paint | Tool settings |
usePhotoBoothStore | ryos:photobooth | Recent photos |
usePcStore | ryos:pc | Virtual PC state |
useStickiesStore | stickies-storage | Sticky notes and note layout |
useCalendarStore | calendar-storage | Calendar events, todo items, groups |
useContactsStore | contacts-storage | Address book contacts, vCard import/export |
useDashboardStore | dashboard-storage | Dashboard widget layout and configuration |
useListenSessionStore | None (ephemeral) | Listen Together session state and Pusher sync |
useInfiniteMacStore | ryos:infinite-mac | Infinite 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
- Window Management - Window state in useAppStore
- File System - useFilesStore details
- Audio System - useAudioSettingsStore details