Move toju-app into own its folder
This commit is contained in:
113
toju-app/src/app/infrastructure/persistence/README.md
Normal file
113
toju-app/src/app/infrastructure/persistence/README.md
Normal 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.
|
||||
Reference in New Issue
Block a user