Move toju-app into own its folder

This commit is contained in:
2026-03-29 23:30:37 +02:00
parent 0467a7b612
commit 8162e0444a
287 changed files with 42 additions and 34 deletions

View File

@@ -0,0 +1,113 @@
# 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.
## 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.