Add gateway realtime fanout and cursor pagination docs
This commit is contained in:
1 parent
4399eafc90
commit
ccf966ffc1
5 files changed
+800
-23
No files matched your search
@@ -0,0 +1,243 @@
|
||||
# Client Integration Guide
|
||||
|
||||
This document describes the current server contract you can rely on when implementing the client.
|
||||
|
||||
## Base Auth Model
|
||||
|
||||
- REST endpoints under `/api/*` require `Authorization: Bearer <jwt>`.
|
||||
- WebSocket gateway at `/gateway` also requires JWT.
|
||||
- Gateway token sources:
|
||||
1. `Authorization: Bearer <jwt>` (preferred when possible)
|
||||
2. query fallback: `?token=<jwt>` or `?access_token=<jwt>` (useful for browser WebSocket clients)
|
||||
|
||||
## Gateway (`/gateway`)
|
||||
|
||||
### Connect
|
||||
|
||||
- URL: `ws://<host>:<port>/gateway` (or `wss://` behind TLS)
|
||||
- On successful auth, server sends:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "ready",
|
||||
"connectionId": "gw_1",
|
||||
"userId": "alice@example.com",
|
||||
"username": "alice"
|
||||
}
|
||||
```
|
||||
|
||||
If auth fails, handshake is rejected with HTTP error (`401`/`403`) and no WebSocket session is established.
|
||||
|
||||
### Client -> Server Messages
|
||||
|
||||
All frames are JSON text.
|
||||
|
||||
#### Subscribe
|
||||
|
||||
```json
|
||||
{"op":"subscribe","channelId":12}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```json
|
||||
{"op":"subscribe","channelIds":[12,13]}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "subscribed",
|
||||
"channelIds": [12, 13],
|
||||
"subscriptions": [12, 13]
|
||||
}
|
||||
```
|
||||
|
||||
If one or more channels do not exist:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "subscribed",
|
||||
"channelIds": [12],
|
||||
"subscriptions": [12],
|
||||
"unknownChannelIds": [999]
|
||||
}
|
||||
```
|
||||
|
||||
#### Unsubscribe
|
||||
|
||||
```json
|
||||
{"op":"unsubscribe","channelId":12}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```json
|
||||
{"op":"unsubscribe","channelIds":[12,13]}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "unsubscribed",
|
||||
"channelIds": [12],
|
||||
"subscriptions": [13]
|
||||
}
|
||||
```
|
||||
|
||||
#### Heartbeat
|
||||
|
||||
Client may send:
|
||||
|
||||
```json
|
||||
{"op":"ping"}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```json
|
||||
{"op":"heartbeat"}
|
||||
```
|
||||
|
||||
Server replies:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "pong",
|
||||
"ts": "2026-02-20T12:34:56.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Server -> Client Event Messages
|
||||
|
||||
After subscription, channel message mutations are pushed as:
|
||||
|
||||
#### Message Created
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "event",
|
||||
"event": "message.created",
|
||||
"channelId": 12,
|
||||
"message": {
|
||||
"id": 101,
|
||||
"content": "hello",
|
||||
"authorId": "alice@example.com",
|
||||
"authorUsername": "alice",
|
||||
"authorAvatarUrl": null,
|
||||
"authorHomeServer": "https://home.example.com",
|
||||
"channelId": 12,
|
||||
"createdAt": "2026-02-20T12:34:56.000",
|
||||
"editedAt": null
|
||||
},
|
||||
"emittedAt": "2026-02-20T12:34:56.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Message Updated
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "event",
|
||||
"event": "message.updated",
|
||||
"channelId": 12,
|
||||
"message": { "...same message shape..." },
|
||||
"emittedAt": "2026-02-20T12:35:56.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Message Deleted
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "event",
|
||||
"event": "message.deleted",
|
||||
"channelId": 12,
|
||||
"messageId": 101,
|
||||
"emittedAt": "2026-02-20T12:36:56.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Gateway Error Payload
|
||||
|
||||
Malformed input or unsupported ops return:
|
||||
|
||||
```json
|
||||
{"op":"error","message":"..."}
|
||||
```
|
||||
|
||||
Common causes:
|
||||
- invalid JSON payload
|
||||
- missing/invalid `op`
|
||||
- unsupported op
|
||||
- missing `channelId`/`channelIds` for subscribe/unsubscribe
|
||||
|
||||
## Message History Pagination (`GET /api/messages`)
|
||||
|
||||
### Query Params
|
||||
|
||||
- `channelId` (required, int)
|
||||
- `limit` (optional, int, default `50`, max `100`)
|
||||
- `before` (optional, message ID cursor)
|
||||
- `after` (optional, message ID cursor)
|
||||
|
||||
Rules:
|
||||
- Do not send `before` and `after` together.
|
||||
- `before`/`after` must be positive integers.
|
||||
|
||||
### Response Shape
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"id": 100,
|
||||
"content": "text",
|
||||
"authorId": "alice@example.com",
|
||||
"authorUsername": "alice",
|
||||
"authorAvatarUrl": null,
|
||||
"authorHomeServer": "https://home.example.com",
|
||||
"channelId": 12,
|
||||
"createdAt": "2026-02-20T12:30:00.000",
|
||||
"editedAt": null
|
||||
}
|
||||
],
|
||||
"channelId": 12,
|
||||
"limit": 50,
|
||||
"hasMore": true,
|
||||
"nextBefore": 51,
|
||||
"nextAfter": 100
|
||||
}
|
||||
```
|
||||
|
||||
### Ordering Semantics
|
||||
|
||||
- no cursor: newest -> oldest (descending by message id)
|
||||
- `before=<id>`: older than cursor, still newest -> oldest
|
||||
- `after=<id>`: newer than cursor, oldest -> newest (ascending)
|
||||
|
||||
For client state:
|
||||
- `nextBefore`: cursor to request older page
|
||||
- `nextAfter`: cursor to request newer page
|
||||
|
||||
## Recommended Client Flow
|
||||
|
||||
1. Login/register/join and store JWT.
|
||||
2. Initial channel load: `GET /api/messages?channelId=<id>&limit=50`.
|
||||
3. Connect `/gateway` with token.
|
||||
4. Send `subscribe` for active channel(s).
|
||||
5. Merge realtime events into local message list:
|
||||
- `message.created`: insert if absent
|
||||
- `message.updated`: patch by id
|
||||
- `message.deleted`: remove/tombstone by id
|
||||
6. Pagination:
|
||||
- load older: call `before=<nextBefore>`
|
||||
- catch up newer (after reconnect): call `after=<lastSeenMessageId>`
|
||||
|
||||
## Practical Notes
|
||||
|
||||
- REST is source of truth; gateway is fanout only.
|
||||
- Gateway registry is currently in-memory on server instance (no cross-instance sync yet).
|
||||
- Channel permissions are not enforced yet by gateway subscriptions; only channel existence is checked.
|
||||
Reference in new issue
Block a user