# Persistence Infrastructure

Offline-first storage layer that keeps messages, users, rooms, reactions, bans, and attachments on the client. The rest of the app only ever talks to `DatabaseService`, which picks the right backend for the current platform at runtime.

## Files

```
persistence/
├── index.ts                        Barrel (exports DatabaseService)
├── database.service.ts              Platform-agnostic facade
├── browser-database.service.ts      IndexedDB backend (web)
└── electron-database.service.ts     IPC/SQLite backend (desktop)
```

## Platform routing

```mermaid
graph TD
    Consumer[Store effects / facades / components]
    Consumer --> Facade[DatabaseService<br/>facade]
    Facade -->|isBrowser?| Decision{Platform}
    Decision -- Browser --> IDB[BrowserDatabaseService<br/>IndexedDB]
    Decision -- Electron --> IPC[ElectronDatabaseService<br/>IPC to main process]
    IPC --> Main[Electron main process<br/>TypeORM + SQLite]

    click Facade "database.service.ts" "DatabaseService - platform-agnostic facade" _blank
    click IDB "browser-database.service.ts" "IndexedDB backend for web" _blank
    click IPC "electron-database.service.ts" "IPC client for Electron" _blank
```

`DatabaseService` is an `@Injectable({ providedIn: 'root' })` that injects both backends and delegates every call to whichever one matches the current platform. Consumers never import a backend directly.

## Object stores / tables

Both backends store the same entity types:

| Store | Key | Indexes | Description |
|---|---|---|---|
| `messages` | `id` | `roomId` | Chat messages, sorted by timestamp |
| `users` | `oderId` | | User profiles |
| `rooms` | `id` | | Server/room metadata |
| `reactions` | `oderId-emoji-messageId` | | Emoji reactions, deduplicated per user |
| `bans` | `oderId` | | Active bans per room |
| `attachments` | `id` | | File/image metadata tied to messages |
| `meta` | `key` | | Key-value pairs (e.g. `currentUserId`) |

The IndexedDB schema is at version 2.

The persisted `rooms` store is a local cache of room metadata. Channel topology is still server-owned metadata: after room create, join, view, or channel-management changes, the renderer should hydrate the authoritative mixed text-and-voice channel list from server-directory responses so every member converges on the same room structure.

## How the two backends differ

### Browser (IndexedDB)

All operations run inside IndexedDB transactions in the renderer thread. Queries like `getMessages` pull all messages for a room via the `roomId` index, sort them by timestamp in JS, then apply limit/offset. Deleted messages are normalised on read (content replaced with a sentinel string).

```mermaid
sequenceDiagram
    participant Eff as NgRx Effect
    participant DB as DatabaseService
    participant BDB as BrowserDatabaseService
    participant IDB as IndexedDB

    Eff->>DB: getMessages(roomId, 50)
    DB->>BDB: getMessages(roomId, 50)
    BDB->>IDB: tx.objectStore("messages")<br/>.index("roomId").getAll(roomId)
    IDB-->>BDB: Message[]
    Note over BDB: Sort by timestamp, slice, normalise
    BDB-->>DB: Message[]
    DB-->>Eff: Message[]
```

### Electron (SQLite via IPC)

The renderer sends structured command/query objects through the Electron preload bridge. The main process handles them with TypeORM against a local SQLite file. No database logic runs in the renderer.

```mermaid
sequenceDiagram
    participant Eff as NgRx Effect
    participant DB as DatabaseService
    participant EDB as ElectronDatabaseService
    participant IPC as Preload Bridge
    participant Main as Main Process<br/>TypeORM + SQLite

    Eff->>DB: saveMessage(msg)
    DB->>EDB: saveMessage(msg)
    EDB->>IPC: api.command({type: "save-message", payload: {message}})
    IPC->>Main: ipcRenderer.invoke
    Main-->>IPC: void
    IPC-->>EDB: Promise resolves
    EDB-->>DB: void
    DB-->>Eff: void
```

The Electron backend's `initialize()` is a no-op because the main process creates the database before the renderer window opens.

## API surface

Every method on `DatabaseService` maps 1:1 to both backends:

**Messages**: `saveMessage`, `getMessages`, `getMessageById`, `deleteMessage`, `updateMessage`, `clearRoomMessages`

**Reactions**: `saveReaction`, `removeReaction`, `getReactionsForMessage`

**Users**: `saveUser`, `getUser`, `getCurrentUser`, `setCurrentUserId`, `getUsersByRoom`, `updateUser`

**Rooms**: `saveRoom`, `getRoom`, `getAllRooms`, `deleteRoom`, `updateRoom`

**Bans**: `saveBan`, `removeBan`, `getBansForRoom`, `isUserBanned`

**Attachments**: `saveAttachment`, `getAttachmentsForMessage`, `getAllAttachments`, `deleteAttachmentsForMessage`

**Lifecycle**: `initialize`, `clearAllData`

The facade also exposes an `isReady` signal that flips to `true` after `initialize()` completes, so components can gate rendering until the DB is available.
