docs: update specs for S2S DM unification and typing relay
This commit is contained in:
@@ -151,6 +151,8 @@ getChannelOrigin(channelId): string // Returns '' for home, origin URL for re
|
||||
|
||||
Built during `populateFromReady()` when WS ready events arrive from each instance.
|
||||
|
||||
> **DM channels** are always mapped to `''` (home origin). `channelOriginMap` is only relevant for Space channels. DM operations always route to the home instance; S2S relay handles cross-instance distribution.
|
||||
|
||||
### API Client Resolution
|
||||
|
||||
```typescript
|
||||
@@ -199,7 +201,7 @@ When a WS connection opens and authenticates, the server sends a `ready` event c
|
||||
2. Merge into the unified space list (replacing stale data from same origin)
|
||||
3. Build/update `channelOriginMap`, `channelToSpaceMap`
|
||||
4. Normalize remote asset URLs to absolute paths
|
||||
5. Deduplicate 1-on-1 DMs that appear from multiple origins (prefer home)
|
||||
5. Skip DM channels from remote origins — DMs are managed exclusively by the home instance
|
||||
6. Last-write-wins layout merge for sidebar order
|
||||
|
||||
---
|
||||
@@ -261,7 +263,5 @@ Client-side and S2S federation serve different purposes:
|
||||
1. User adds a remote instance via Connections (client-side)
|
||||
2. The client triggers S2S peering between the two servers (automatic)
|
||||
3. User joins Spaces on the remote instance (client-side — API calls go directly to remote)
|
||||
4. User sends DMs (messages go to the appropriate server, S2S relay distributes to peers)
|
||||
4. User sends DMs — all DM writes go to the home instance. S2S relay distributes messages, reactions, and membership changes to peer instances.
|
||||
5. Friend requests and discovery work across instances (client loads friends from all connected instances, S2S relays friend events)
|
||||
|
||||
> **Note on DMs (2026-04-01):** DM operations currently use a dual path — the client may route DM writes to the remote instance directly (client-side) or through the home instance (S2S relay). This is being unified to S2S-only for DMs. See the S2S DM Unification project.
|
||||
|
||||
@@ -79,6 +79,8 @@ The format difference (32-char hex vs 36-char UUID with dashes) allows detecting
|
||||
|
||||
**No federation event queued at creation time.** The `federatedId` for 1-on-1 DMs is computed on demand when the first message is relayed via `queueDmRelay()`. The receiving instance uses `findOrCreateDmChannel()` which computes the deterministic hash and creates the channel if needed.
|
||||
|
||||
**Client routing:** DM creation always goes to the home instance. For federated users, the client passes `{ homeUserId, homeInstance }` and the server resolves the target via `resolveOrCreateReplicatedUser()`. The `federatedId` is computed at creation time when either participant has `homeInstance` set.
|
||||
|
||||
---
|
||||
|
||||
## Group DM Creation
|
||||
@@ -603,10 +605,10 @@ const normalized = homeInstance.startsWith('http')
|
||||
| Method | Path | Auth | Purpose |
|
||||
|--------|------|------|---------|
|
||||
| `GET` | `/api/dm` | JWT | List caller's DM channels (excludes `closed=1` and `deleted_at` IS NOT NULL) |
|
||||
| `POST` | `/api/dm` | JWT | Create or get existing 1-on-1 DM |
|
||||
| `POST` | `/api/dm` | JWT | Create or get existing 1-on-1 DM. Accepts `{ userId }` (local) or `{ homeUserId, homeInstance }` (federated) |
|
||||
| `POST` | `/api/dm/group` | JWT | Create group DM with multiple members |
|
||||
| `DELETE` | `/api/dm/:id` | JWT | Soft-close DM for caller |
|
||||
| `POST` | `/api/dm/:id/members` | JWT | Add member to group DM (owner only) |
|
||||
| `POST` | `/api/dm/:id/members` | JWT | Add member to group DM (owner only). Accepts `{ userId }` or `{ homeUserId, homeInstance }` |
|
||||
| `DELETE` | `/api/dm/:id/members` | JWT | Leave group DM |
|
||||
| `GET` | `/api/dm/:id/messages` | JWT | Get messages with cursor pagination |
|
||||
| `POST` | `/api/dm/:id/messages` | JWT | Send message (rate-limited: 5/5s) |
|
||||
@@ -647,6 +649,7 @@ For full wire formats, see `docs/systems/websocket.md`.
|
||||
| `dm_message_created` | S->C | New message (user or system) |
|
||||
| `dm_message_updated` | S->C | Message edit |
|
||||
| `dm_message_deleted` | S->C | Message delete |
|
||||
| `dm_typing_stop` | S->C | Message send (clears indicator immediately) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -312,6 +312,28 @@ Every relayed message is stored with:
|
||||
|
||||
The `(source_instance, source_message_id)` pair is checked before insertion. Duplicates are rejected with reason `'duplicate'`. A unique partial index enforces this at the DB level: `idx_dm_messages_source_unique ON dm_messages(source_instance, source_message_id) WHERE source_instance IS NOT NULL`.
|
||||
|
||||
### Typing Indicator Relay
|
||||
|
||||
**Event types:** `dm_typing_start`, `dm_typing_stop`
|
||||
|
||||
**Model:** Fire-and-forget, same as call signaling. No outbox, no retry, no mutation log. Typing is ephemeral — lost packets are acceptable.
|
||||
|
||||
**Channel identification:** Uses `federatedId` (not instance-local `dmChannelId`) for cross-instance channel lookup, plus `participants` for resolution context.
|
||||
|
||||
**Outbound (`events.ts` / `dm.ts`):**
|
||||
- `handleDmTypingStart()` → after local broadcast, calls `sendTypingRelay(dmChannelId, 'dm_typing_start', userId)`
|
||||
- `broadcastDmMessage()` → after local `dm_typing_stop` broadcast, calls `sendTypingRelay(dmChannelId, 'dm_typing_stop', message.userId)`
|
||||
|
||||
**`sendTypingRelay()` (`federationOutbox.ts`):**
|
||||
- Fetches channel's `federatedId` and `getDmParticipants()` for target resolution
|
||||
- Builds `FederationRelayEvent` with `typing: { homeUserId, homeInstance, username }`
|
||||
- Reuses `sendCallRelay()` for the actual POST to each remote peer origin
|
||||
|
||||
**Inbound (`federation.ts`):**
|
||||
- `processDmTypingStartEvent` → look up channel by `federatedId`, resolve user via `resolveLocalUser()` (no stub creation for ephemeral events), broadcast `dm_typing` to local members
|
||||
- `processDmTypingStopEvent` → same, broadcast `dm_typing_stop` to local members
|
||||
- **Implicit clear:** `processCreateEvent()` also emits `dm_typing_stop` for the message author after processing an inbound relay — primary typing clear mechanism for relayed messages
|
||||
|
||||
---
|
||||
|
||||
## 5. Outbox & Relay Pipeline
|
||||
@@ -425,6 +447,8 @@ Body limit: 10 MB. Max 50 events per batch. Rate-limited to 90 requests/min per
|
||||
| `friend_add` | `processFriendAddEvent` | friend |
|
||||
| `friend_remove` | `processFriendRemoveEvent` | friend |
|
||||
| `file_rejected` | `processFileRejectedEvent` | dm |
|
||||
| `dm_typing_start` | `processDmTypingStartEvent` | dm (fire-and-forget, no outbox) |
|
||||
| `dm_typing_stop` | `processDmTypingStopEvent` | dm (fire-and-forget, no outbox) |
|
||||
|
||||
After processing all events, the relay endpoint updates the peer's `lastSeenAt` and resets `consecutiveFailures`, then returns accepted/rejected arrays plus `maxUploadSize`.
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ Source: `packages/server/src/ws/handler.ts`, `packages/server/src/ws/events.ts`
|
||||
| `dm_message_updated` | message: DmMessageWithUser | DM members |
|
||||
| `dm_message_deleted` | messageId, dmChannelId | DM members |
|
||||
| `dm_typing` | dmChannelId, userId, username | DM members (excludes sender) |
|
||||
| `dm_typing_stop` | dmChannelId, userId | DM members (excludes typer) |
|
||||
| `dm_embeds_resolved` | messageId, dmChannelId, embeds[] | DM members |
|
||||
|
||||
### Read State
|
||||
|
||||
Reference in New Issue
Block a user