docs: update README schema section to match actual database schema
All checks were successful
check / check (push) Successful in 1m11s
All checks were successful
check / check (push) Successful in 1m11s
Update the Schema section and related references throughout README.md to accurately reflect the current 001_initial.sql migration: - Rename 'users' table to 'sessions' with new columns: uuid, password_hash, signing_key, away_message - Add new 'clients' table (uuid, session_id FK, token, created_at, last_seen) - Add topic_set_by and topic_set_at columns to 'channels' table - Update channel_members FK from user_id to session_id - Add params column to messages table - Update client_queues FK from user_id to client_id - Update Queue Architecture diagram labels and surrounding text - Update In-Memory Broker description to use client_id terminology - Update Multi-Client Model MVP note to reflect sessions/clients split
This commit is contained in:
102
README.md
102
README.md
@@ -207,12 +207,12 @@ User Session
|
|||||||
└── Client C (token_c, queue_c)
|
└── Client C (token_c, queue_c)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Current MVP note:** The current implementation creates a new user (with new
|
**Current MVP note:** The current implementation creates a new session (with new
|
||||||
nick) per `POST /api/v1/session` call. True multi-client (multiple tokens
|
nick) per `POST /api/v1/session` call. True multi-client (multiple clients
|
||||||
sharing one nick/session) is supported by the schema (`client_queues` is keyed
|
sharing one session/nick) is supported by the schema — `clients` references
|
||||||
by user_id, and multiple tokens can point to the same user) but the session
|
`sessions` via `session_id`, and `client_queues` is keyed by `client_id` — but
|
||||||
creation endpoint does not yet support "add a client to an existing session."
|
the session creation endpoint does not yet support "add a client to an existing
|
||||||
This will be added post-MVP.
|
session." This will be added post-MVP.
|
||||||
|
|
||||||
**Rationale:** The fundamental IRC mobile problem is that you can't have your
|
**Rationale:** The fundamental IRC mobile problem is that you can't have your
|
||||||
phone and laptop connected simultaneously without a bouncer. Server-side
|
phone and laptop connected simultaneously without a bouncer. Server-side
|
||||||
@@ -265,8 +265,8 @@ The server implements HTTP long-polling for real-time message delivery:
|
|||||||
- The client disconnects (connection closed, no response needed)
|
- The client disconnects (connection closed, no response needed)
|
||||||
|
|
||||||
**Implementation detail:** The server maintains an in-memory broker with
|
**Implementation detail:** The server maintains an in-memory broker with
|
||||||
per-user notification channels. When a message is enqueued for a user, the
|
per-client notification channels. When a message is enqueued for a client, the
|
||||||
broker closes all waiting channels for that user, waking up any blocked
|
broker closes all waiting channels for that client, waking up any blocked
|
||||||
long-poll handlers. This is O(1) notification — no polling loops, no database
|
long-poll handlers. This is O(1) notification — no polling loops, no database
|
||||||
scanning.
|
scanning.
|
||||||
|
|
||||||
@@ -398,28 +398,28 @@ The entire read/write loop for a client is two endpoints. Everything else
|
|||||||
│ │ │
|
│ │ │
|
||||||
┌─────────▼──┐ ┌───────▼────┐ ┌──────▼─────┐
|
┌─────────▼──┐ ┌───────▼────┐ ┌──────▼─────┐
|
||||||
│client_queue│ │client_queue│ │client_queue│
|
│client_queue│ │client_queue│ │client_queue│
|
||||||
│ user_id=1 │ │ user_id=2 │ │ user_id=3 │
|
│ client_id=1│ │ client_id=2│ │ client_id=3│
|
||||||
│ msg_id=N │ │ msg_id=N │ │ msg_id=N │
|
│ msg_id=N │ │ msg_id=N │ │ msg_id=N │
|
||||||
└────────────┘ └────────────┘ └────────────┘
|
└────────────┘ └────────────┘ └────────────┘
|
||||||
alice bob carol
|
alice bob carol
|
||||||
|
|
||||||
Each message is stored ONCE. One queue entry per recipient.
|
Each message is stored ONCE. One queue entry per recipient client.
|
||||||
```
|
```
|
||||||
|
|
||||||
The `client_queues` table contains `(user_id, message_id)` pairs. When a
|
The `client_queues` table contains `(client_id, message_id)` pairs. When a
|
||||||
client polls with `GET /messages?after=<queue_id>`, the server queries for
|
client polls with `GET /messages?after=<queue_id>`, the server queries for
|
||||||
queue entries with `id > after` for that user, joins against the messages
|
queue entries with `id > after` for that client, joins against the messages
|
||||||
table, and returns the results. The `queue_id` (auto-incrementing primary
|
table, and returns the results. The `queue_id` (auto-incrementing primary
|
||||||
key of `client_queues`) serves as a monotonically increasing cursor.
|
key of `client_queues`) serves as a monotonically increasing cursor.
|
||||||
|
|
||||||
### In-Memory Broker
|
### In-Memory Broker
|
||||||
|
|
||||||
The server maintains an in-memory notification broker to avoid database
|
The server maintains an in-memory notification broker to avoid database
|
||||||
polling. The broker is a map of `user_id → []chan struct{}`. When a message
|
polling. The broker is a map of `client_id → []chan struct{}`. When a message
|
||||||
is enqueued for a user:
|
is enqueued for a client:
|
||||||
|
|
||||||
1. The handler calls `broker.Notify(userID)`
|
1. The handler calls `broker.Notify(clientID)`
|
||||||
2. The broker closes all waiting channels for that user
|
2. The broker closes all waiting channels for that client
|
||||||
3. Any goroutines blocked in `select` on those channels wake up
|
3. Any goroutines blocked in `select` on those channels wake up
|
||||||
4. The woken handler queries the database for new queue entries
|
4. The woken handler queries the database for new queue entries
|
||||||
5. Messages are returned to the client
|
5. Messages are returned to the client
|
||||||
@@ -1740,33 +1740,52 @@ The database schema is managed via embedded SQL migration files in
|
|||||||
|
|
||||||
**Current tables:**
|
**Current tables:**
|
||||||
|
|
||||||
#### `users`
|
#### `sessions`
|
||||||
| Column | Type | Description |
|
| Column | Type | Description |
|
||||||
|-------------|----------|-------------|
|
|-----------------|----------|-------------|
|
||||||
| `id` | INTEGER | Primary key (auto-increment) |
|
| `id` | INTEGER | Primary key (auto-increment) |
|
||||||
| `nick` | TEXT | Unique nick |
|
| `uuid` | TEXT | Unique session UUID |
|
||||||
| `token` | TEXT | Unique auth token (64 hex chars) |
|
| `nick` | TEXT | Unique nick |
|
||||||
| `created_at`| DATETIME | Session creation time |
|
| `password_hash` | TEXT | Password hash (default empty) |
|
||||||
| `last_seen` | DATETIME | Last API request time |
|
| `signing_key` | TEXT | Ed25519 signing key (default empty) |
|
||||||
|
| `away_message` | TEXT | Away message (default empty) |
|
||||||
|
| `created_at` | DATETIME | Session creation time |
|
||||||
|
| `last_seen` | DATETIME | Last API request time |
|
||||||
|
|
||||||
|
Index on `(uuid)`.
|
||||||
|
|
||||||
|
#### `clients`
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------------|----------|-------------|
|
||||||
|
| `id` | INTEGER | Primary key (auto-increment) |
|
||||||
|
| `uuid` | TEXT | Unique client UUID |
|
||||||
|
| `session_id` | INTEGER | FK → sessions.id (cascade delete) |
|
||||||
|
| `token` | TEXT | Unique auth token (64 hex chars) |
|
||||||
|
| `created_at` | DATETIME | Client creation time |
|
||||||
|
| `last_seen` | DATETIME | Last API request time |
|
||||||
|
|
||||||
|
Indexes on `(token)` and `(session_id)`.
|
||||||
|
|
||||||
#### `channels`
|
#### `channels`
|
||||||
| Column | Type | Description |
|
| Column | Type | Description |
|
||||||
|-------------|----------|-------------|
|
|---------------|----------|-------------|
|
||||||
| `id` | INTEGER | Primary key (auto-increment) |
|
| `id` | INTEGER | Primary key (auto-increment) |
|
||||||
| `name` | TEXT | Unique channel name (e.g., `#general`) |
|
| `name` | TEXT | Unique channel name (e.g., `#general`) |
|
||||||
| `topic` | TEXT | Channel topic (default empty) |
|
| `topic` | TEXT | Channel topic (default empty) |
|
||||||
| `created_at`| DATETIME | Channel creation time |
|
| `topic_set_by`| TEXT | Nick of the user who set the topic (default empty) |
|
||||||
| `updated_at`| DATETIME | Last modification time |
|
| `topic_set_at`| DATETIME | When the topic was last set |
|
||||||
|
| `created_at` | DATETIME | Channel creation time |
|
||||||
|
| `updated_at` | DATETIME | Last modification time |
|
||||||
|
|
||||||
#### `channel_members`
|
#### `channel_members`
|
||||||
| Column | Type | Description |
|
| Column | Type | Description |
|
||||||
|-------------|----------|-------------|
|
|--------------|----------|-------------|
|
||||||
| `id` | INTEGER | Primary key (auto-increment) |
|
| `id` | INTEGER | Primary key (auto-increment) |
|
||||||
| `channel_id`| INTEGER | FK → channels.id |
|
| `channel_id` | INTEGER | FK → channels.id (cascade delete) |
|
||||||
| `user_id` | INTEGER | FK → users.id |
|
| `session_id` | INTEGER | FK → sessions.id (cascade delete) |
|
||||||
| `joined_at` | DATETIME | When the user joined |
|
| `joined_at` | DATETIME | When the user joined |
|
||||||
|
|
||||||
Unique constraint on `(channel_id, user_id)`.
|
Unique constraint on `(channel_id, session_id)`.
|
||||||
|
|
||||||
#### `messages`
|
#### `messages`
|
||||||
| Column | Type | Description |
|
| Column | Type | Description |
|
||||||
@@ -1776,6 +1795,7 @@ Unique constraint on `(channel_id, user_id)`.
|
|||||||
| `command` | TEXT | IRC command (`PRIVMSG`, `JOIN`, etc.) |
|
| `command` | TEXT | IRC command (`PRIVMSG`, `JOIN`, etc.) |
|
||||||
| `msg_from` | TEXT | Sender nick |
|
| `msg_from` | TEXT | Sender nick |
|
||||||
| `msg_to` | TEXT | Target (`#channel` or nick) |
|
| `msg_to` | TEXT | Target (`#channel` or nick) |
|
||||||
|
| `params` | TEXT | JSON-encoded IRC-style positional parameters |
|
||||||
| `body` | TEXT | JSON-encoded body (array or object) |
|
| `body` | TEXT | JSON-encoded body (array or object) |
|
||||||
| `meta` | TEXT | JSON-encoded metadata |
|
| `meta` | TEXT | JSON-encoded metadata |
|
||||||
| `created_at`| DATETIME | Server timestamp |
|
| `created_at`| DATETIME | Server timestamp |
|
||||||
@@ -1786,11 +1806,11 @@ Indexes on `(msg_to, id)` and `(created_at)`.
|
|||||||
| Column | Type | Description |
|
| Column | Type | Description |
|
||||||
|-------------|----------|-------------|
|
|-------------|----------|-------------|
|
||||||
| `id` | INTEGER | Primary key (auto-increment). Used as the poll cursor. |
|
| `id` | INTEGER | Primary key (auto-increment). Used as the poll cursor. |
|
||||||
| `user_id` | INTEGER | FK → users.id |
|
| `client_id` | INTEGER | FK → clients.id (cascade delete) |
|
||||||
| `message_id`| INTEGER | FK → messages.id |
|
| `message_id`| INTEGER | FK → messages.id (cascade delete) |
|
||||||
| `created_at`| DATETIME | When the entry was queued |
|
| `created_at`| DATETIME | When the entry was queued |
|
||||||
|
|
||||||
Unique constraint on `(user_id, message_id)`. Index on `(user_id, id)`.
|
Unique constraint on `(client_id, message_id)`. Index on `(client_id, id)`.
|
||||||
|
|
||||||
The `client_queues.id` is the monotonically increasing cursor used by
|
The `client_queues.id` is the monotonically increasing cursor used by
|
||||||
`GET /messages?after=<id>`. This is more reliable than timestamps (no clock
|
`GET /messages?after=<id>`. This is more reliable than timestamps (no clock
|
||||||
|
|||||||
Reference in New Issue
Block a user