Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions api/sync/auto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
isIndividualBlobSyncDomain,
type BlobSyncDomain,
} from "../../src/utils/cloudSyncShared.js";
import { normalizeCloudSyncRevision } from "../../src/utils/cloudSyncRevision.js";
import {
normalizeDeletionMarkerMap,
type DeletionMarkerMap,
Expand Down Expand Up @@ -56,6 +57,7 @@ interface SaveAutoSyncMetadataBody {
blobUrl?: string;
updatedAt?: string;
version?: number;
baseVersion?: number;
totalSize?: number;
items?: Record<string, PersistedAutoSyncItemMetadata>;
deletedItems?: DeletionMarkerMap;
Expand Down Expand Up @@ -95,6 +97,9 @@ function normalizePersistedItemMetadata(
updatedAt: candidate.updatedAt,
signature: candidate.signature,
size: candidate.size,
...(normalizeCloudSyncRevision(candidate.revision)
? { revision: normalizeCloudSyncRevision(candidate.revision) }
: {}),
storageUrl,
blobUrl: storageUrl,
};
Expand Down Expand Up @@ -243,7 +248,30 @@ async function handleSaveMetadata(
try {
const existing = await readPersistedMetadata(redis, username);
const previous = existing[body.domain];
const currentVersion = previous?.version ?? 0;
const requestedBaseVersion =
typeof body.baseVersion === "number" && Number.isFinite(body.baseVersion)
? body.baseVersion
: null;

if (requestedBaseVersion === null) {
if (currentVersion > 0) {
res.status(409).json({
error: "sync_conflict: stale baseVersion",
currentVersion,
});
return;
}
} else if (requestedBaseVersion !== currentVersion) {
res.status(409).json({
error: "sync_conflict: stale baseVersion",
currentVersion,
});
return;
}

const createdAt = previous?.createdAt || new Date().toISOString();
const nextVersion = Math.max(currentVersion + 1, AUTO_SYNC_SNAPSHOT_VERSION);
const legacyStorageUrl = getStoredLocation(body);

if (body.items) {
Expand Down Expand Up @@ -327,7 +355,7 @@ async function handleSaveMetadata(

existing[body.domain] = {
updatedAt: body.updatedAt,
version: body.version || AUTO_SYNC_SNAPSHOT_VERSION,
version: nextVersion,
totalSize:
body.totalSize ||
Object.values(nextItems).reduce((sum, item) => sum + item.size, 0),
Expand Down Expand Up @@ -373,7 +401,7 @@ async function handleSaveMetadata(

existing[body.domain] = {
updatedAt: body.updatedAt,
version: body.version || AUTO_SYNC_SNAPSHOT_VERSION,
version: nextVersion,
totalSize: body.totalSize || objectInfo.size,
storageUrl: legacyStorageUrl,
blobUrl: legacyStorageUrl,
Expand Down Expand Up @@ -448,6 +476,7 @@ async function handleDomainDownload(
updatedAt: itemValue.updatedAt,
signature: itemValue.signature,
size: itemValue.size || objectInfo.size,
...(itemValue.revision ? { revision: itemValue.revision } : {}),
storageUrl,
downloadUrl: await createSignedDownloadUrl(storageUrl),
};
Expand All @@ -465,6 +494,7 @@ async function handleDomainDownload(
updatedAt: item.updatedAt,
signature: item.signature,
size: item.size,
...(item.revision ? { revision: item.revision } : {}),
storageUrl: item.storageUrl,
blobUrl: item.storageUrl,
},
Expand Down
29 changes: 27 additions & 2 deletions api/sync/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface PutStateBody {
data?: unknown;
updatedAt?: string;
version?: number;
baseVersion?: number;
}

function isContactsSnapshotData(value: unknown): value is { contacts: unknown[] } {
Expand Down Expand Up @@ -399,6 +400,29 @@ async function handlePutState(
}

const domain = body.domain as RedisSyncDomain;
const currentMeta = await readMetaMap(redis, username);
const currentVersion = currentMeta[domain]?.version ?? 0;
const requestedBaseVersion =
typeof body.baseVersion === "number" && Number.isFinite(body.baseVersion)
? body.baseVersion
: null;

if (requestedBaseVersion === null) {
if (currentVersion > 0) {
res.status(409).json({
error: "sync_conflict: stale baseVersion",
currentVersion,
});
return;
}
} else if (requestedBaseVersion !== currentVersion) {
res.status(409).json({
error: "sync_conflict: stale baseVersion",
currentVersion,
});
return;
}

if (domain === "contacts" && !isContactsSnapshotData(body.data)) {
res.status(400).json({
error: "Invalid contacts snapshot payload",
Expand All @@ -413,11 +437,12 @@ async function handlePutState(
}

const now = new Date().toISOString();
const nextVersion = Math.max(currentVersion + 1, AUTO_SYNC_SNAPSHOT_VERSION);

const entry: PersistedRedisStateDomain = {
data: body.data,
updatedAt: body.updatedAt,
version: body.version || AUTO_SYNC_SNAPSHOT_VERSION,
version: nextVersion,
createdAt: now,
};

Expand All @@ -430,7 +455,7 @@ async function handlePutState(
body.data as SongsSnapshotData,
{
updatedAt: entry.updatedAt,
version: entry.version,
version: nextVersion,
createdAt: entry.createdAt,
}
);
Expand Down
10 changes: 5 additions & 5 deletions docs/1-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ graph TB
end

subgraph Data["Data Layer"]
IndexedDB[(IndexedDB)]
OPFS[(OPFS)]
LocalStorage[(LocalStorage)]
API[Vercel + Bun API]
end
Expand All @@ -31,7 +31,7 @@ graph TB

Apps --> Zustand
UI --> Zustand
Zustand --> IndexedDB
Zustand --> OPFS
Zustand --> LocalStorage
Zustand --> API
API --> AI
Expand All @@ -56,7 +56,7 @@ graph TB
- **[Multi-Theme Support](/docs/theme-system):** System 7, Mac OS X (Aqua), Windows XP, Windows 98
- **[23 Built-in Apps](/docs/apps):** Finder, TextEdit, Paint, iPod, Infinite Mac, Winamp, Calendar, Dashboard, Contacts, and more
- **[AI Assistant (Ryo)](/docs/ai-system):** Chat, tool calling, app control, code generation
- **[Virtual File System](/docs/file-system):** IndexedDB-backed with lazy loading and cloud sync
- **[Virtual File System](/docs/file-system):** OPFS-backed with lazy loading and cloud sync
- **[Real-time Chat](/docs/rooms-api):** RESTful rooms with AI integration
- **[Audio System](/docs/audio-system):** Synthesizer, soundboard, TTS, and UI sounds
- **[Component Library](/docs/component-library):** shadcn/ui + custom components with i18n
Expand All @@ -70,11 +70,11 @@ graph TB
| Category | Technologies |
|----------|-------------|
| Frontend | React 19, TypeScript, Tailwind CSS v4, shadcn/ui, Framer Motion |
| State | Zustand with localStorage/IndexedDB persistence |
| State | Zustand with localStorage/OPFS-backed persistence |
| Audio | Tone.js, WaveSurfer.js, Web Audio API |
| 3D | Three.js (shaders) |
| Text Editor | TipTap |
| Storage | IndexedDB, LocalStorage, Redis (Upstash REST / standard), Vercel Blob / S3-compatible |
| Storage | OPFS, LocalStorage, Redis (Upstash REST / standard), Vercel Blob / S3-compatible |
| API Runtime | Vercel Node.js handlers + standalone Bun server |
| AI | OpenAI, Anthropic, Google via Vercel AI SDK |
| Real-time | Pusher or local WebSocket (with Redis pub/sub fanout) |
Expand Down
2 changes: 1 addition & 1 deletion docs/1.1-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ graph TB

subgraph Persistence["Persistence Layer"]
LocalStorage[(localStorage)]
IndexedDB[(IndexedDB)]
OPFS[(OPFS)]
end

subgraph API["API Layer (Vercel Node.js + standalone Bun server)"]
Expand Down
4 changes: 2 additions & 2 deletions docs/2-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ All apps follow a consistent architecture pattern:
### State Management
- Apps use **Zustand stores** for global state (e.g., `useIpodStore`, `useSoundboardStore`)
- Local component state for UI-specific concerns
- IndexedDB persistence for user data (songs, soundboards, etc.)
- Browser-storage persistence for user data (including OPFS-backed file content)

### Window Configuration
Each app defines window constraints:
Expand All @@ -60,7 +60,7 @@ Most apps are lazy-loaded for performance:
## Key Features by Category

### File Management
- **Virtual File System**: IndexedDB-backed with lazy loading
- **Virtual File System**: OPFS-backed with lazy loading
- **File Operations**: Create, rename, move, delete files and folders
- **Quick Access**: Jump to Documents, Applications, Trash

Expand Down
8 changes: 4 additions & 4 deletions docs/2.6-finder.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Finder

Finder is the core file management application for ryOS, providing users with a familiar interface to browse, organize, and manage their files within a secure, virtualized file system powered by IndexedDB. It serves as your primary interaction point for all documents, applications, and data stored within your ryOS environment.
Finder is the core file management application for ryOS, providing users with a familiar interface to browse, organize, and manage their files within a secure, virtualized file system backed by OPFS. It serves as your primary interaction point for all documents, applications, and data stored within your ryOS environment.

## Overview

The ryOS Finder app acts as the central hub for all file-related operations, offering an intuitive and efficient way to navigate your digital workspace. It allows users to explore a hierarchical file system, providing a desktop-like experience for managing personal and system files. By leveraging IndexedDB, Finder ensures that your data is persistently stored and quickly accessible across sessions, maintaining a consistent and reliable user experience. Designed for both simplicity and power, Finder streamlines file organization, from creating new directories to managing the trash, empowering users with essential tools for a productive workflow within ryOS.
The ryOS Finder app acts as the central hub for all file-related operations, offering an intuitive and efficient way to navigate your digital workspace. It allows users to explore a hierarchical file system, providing a desktop-like experience for managing personal and system files. By leveraging OPFS-backed browser storage, Finder ensures that your data is persistently stored and quickly accessible across sessions, maintaining a consistent and reliable user experience. Designed for both simplicity and power, Finder streamlines file organization, from creating new directories to managing the trash, empowering users with essential tools for a productive workflow within ryOS.

## Features

Expand Down Expand Up @@ -61,7 +61,7 @@ The app consists of 4 component file(s):

### Hooks & Utilities
**Custom Hooks:**
- `src/apps/finder/hooks/useFileSystem.ts`: This custom hook encapsulates the core logic for interacting with the IndexedDB-backed virtual file system. It provides a clean API for performing CRUD (Create, Read, Update, Delete) operations on files and folders, handling path resolution, and managing file system state.
- `src/apps/finder/hooks/useFileSystem.ts`: This custom hook encapsulates the core logic for interacting with the OPFS-backed virtual file system. It provides a clean API for performing CRUD (Create, Read, Update, Delete) operations on files and folders, handling path resolution, and managing file system state.

**Utility Files:**
No custom utility files are explicitly defined for this app.
Expand All @@ -70,7 +70,7 @@ No custom utility files are explicitly defined for this app.
The Finder app employs a robust state management strategy combining global and local approaches:
* **Zustand Stores:** It heavily utilizes Zustand for global state management, including `useFinderStore` for app-specific settings like the current path, view type, and sort order; `useFilesStore` for broader file-related data; `useAppStore` for overall application lifecycle; and `useThemeStore` for user interface theme preferences.
* **Local Component State:** Standard React `useState` and `useRef` hooks are used within individual components (e.g., `FinderAppComponent`, `FileList`) to manage ephemeral UI state, such as dialog visibility, selected items, input field values, and other component-specific interactions.
* **`useFileSystem` Hook:** This custom hook acts as a specialized state manager for the file system itself, abstracting away the complexities of IndexedDB interactions and providing a consistent interface for file system manipulation.
* **`useFileSystem` Hook:** This custom hook acts as a specialized state manager for the file system itself, abstracting away the complexities of browser content storage interactions and providing a consistent interface for file system manipulation.

## Related Apps

Expand Down
19 changes: 9 additions & 10 deletions docs/3.2-state-management.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# State Management

ryOS uses Zustand for state management with 30 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.
ryOS uses Zustand for state management with 30 stores following consistent patterns, combined with localStorage and OPFS-backed browser storage 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

Expand Down Expand Up @@ -31,7 +31,7 @@ flowchart TB

subgraph "Persistence Layer"
LS[(localStorage)]
IDB[(IndexedDB)]
OPFS[(OPFS)]
end

subgraph "External"
Expand All @@ -49,9 +49,9 @@ flowchart TB

APP --> LS
FILES --> LS
FILES --> IDB
FILES --> OPFS
CHAT --> LS
DISPLAY --> IDB
DISPLAY --> OPFS

IPOD --> API
CHAT --> API
Expand Down Expand Up @@ -233,12 +233,12 @@ persist(
)
```

### IndexedDB Integration
### OPFS-backed Browser Storage

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

```typescript
// IndexedDB stores
// Browser content stores
const STORES = {
DOCUMENTS: "documents", // Text files
IMAGES: "images", // Binary images
Expand All @@ -247,7 +247,6 @@ const STORES = {
CUSTOM_WALLPAPERS: "custom_wallpapers",
};

// Database: ryOS (version 7)
interface StoredContent {
name: string; // Original filename
content: string | Blob; // File content
Expand All @@ -264,7 +263,7 @@ flowchart TB
FS[File System Items<br/>path, name, type, uuid, status]
end

subgraph "Content Layer (IndexedDB)"
subgraph "Content Layer (OPFS)"
DOC[documents store]
IMG[images store]
APP[applets store]
Expand Down Expand Up @@ -500,7 +499,7 @@ sequenceDiagram
participant R as Rehydrate
participant S as Store
participant LS as localStorage
participant IDB as IndexedDB
participant OPFS as OPFS
participant API as External API

R->>LS: Load persisted state
Expand All @@ -511,7 +510,7 @@ sequenceDiagram
S->>API: loadDefaultFiles()
API-->>S: JSON data
S->>LS: Persist metadata
S->>IDB: Save text content
S->>OPFS: Save text content
S->>S: registerFilesForLazyLoad(assets)
else libraryState === "loaded"
S->>S: syncRootDirectories()
Expand Down
Loading
Loading