docs(systems): document three-way ack classification + no_recipient (#18)
federation.md — new undeliverable bucket subsection with three-way classification table, Path A/B semantics, and wire backward-compat note. voice.md — no_recipient row in failure-surface table. websocket.md — DmCallUndeliverableReason union updated to include no_recipient. dm-system.md — cross-reference to voice.md for no_recipient reason.
This commit is contained in:
@@ -284,6 +284,7 @@ If a `member_add` federation event arrives for a soft-deleted channel (non-null
|
|||||||
**Request:** `{ content?: string, attachments?: string[], replyToId?: string }`
|
**Request:** `{ content?: string, attachments?: string[], replyToId?: string }`
|
||||||
|
|
||||||
**Cross-instance access:** Federated users (those with `homeInstance` set) can send messages on any DM channel where they are a member, regardless of which instance serves the request. The `requireLocalUser` gate that previously blocked federated users from DM write endpoints has been removed. DM calls work across federated instances. The caller's instance hosts the LiveKit room; remote clients connect directly. Call signaling is relayed to all active federation peers via synchronous HTTP POST (not the outbox worker). Relay failures at any call state transition emit `dm_call_undeliverable { phase, terminal, failures }` to the originator — see `docs/systems/voice.md` for the full call state machine and failure surface.
|
**Cross-instance access:** Federated users (those with `homeInstance` set) can send messages on any DM channel where they are a member, regardless of which instance serves the request. The `requireLocalUser` gate that previously blocked federated users from DM write endpoints has been removed. DM calls work across federated instances. The caller's instance hosts the LiveKit room; remote clients connect directly. Call signaling is relayed to all active federation peers via synchronous HTTP POST (not the outbox worker). Relay failures at any call state transition emit `dm_call_undeliverable { phase, terminal, failures }` to the originator — see `docs/systems/voice.md` for the full call state machine and failure surface.
|
||||||
|
- Federated call-start to a remote instance with no reachable recipient surfaces as `dm_call_undeliverable` with reason `no_recipient` — see `voice.md` for the full failure-surface table.
|
||||||
|
|
||||||
**Validation:**
|
**Validation:**
|
||||||
- Caller must be a member (`isDmMember`)
|
- Caller must be a member (`isDmMember`)
|
||||||
|
|||||||
@@ -529,10 +529,42 @@ interface FederationRelayResponse {
|
|||||||
messageId: string;
|
messageId: string;
|
||||||
reason: string; // e.g., 'duplicate', 'unknown_message', 'missing_participants'
|
reason: string; // e.g., 'duplicate', 'unknown_message', 'missing_participants'
|
||||||
}>;
|
}>;
|
||||||
|
undeliverable?: Array<{ // optional — omitted when empty; call-signaling only
|
||||||
|
messageId: string;
|
||||||
|
reason: string; // e.g., 'no_recipient'
|
||||||
|
}>;
|
||||||
maxUploadSize: number; // This instance's max upload size in bytes
|
maxUploadSize: number; // This instance's max upload size in bytes
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `undeliverable` bucket (call-signaling only)
|
||||||
|
|
||||||
|
In addition to `accepted` and `rejected`, the relay response may include an
|
||||||
|
optional `undeliverable: Array<{messageId, reason}>`. Three-way classification,
|
||||||
|
non-overlapping: each messageId appears in exactly one of the three arrays.
|
||||||
|
|
||||||
|
| Bucket | Meaning | Retry? |
|
||||||
|
|---|---|---|
|
||||||
|
| `accepted` | Processed cleanly, ≥1 recipient reached. | No |
|
||||||
|
| `rejected` | Refused at data/protocol layer (schema, attribution, channel-not-found, etc.). | Terminal. |
|
||||||
|
| `undeliverable` | Processed cleanly, zero recipients reachable. | No — call-signaling specific. |
|
||||||
|
|
||||||
|
Currently used only for `dm_call_start`:
|
||||||
|
- **Path A** (local DM exists): if no local non-caller member has an active WS
|
||||||
|
connection, the event is pushed to `undeliverable` with reason `no_recipient`
|
||||||
|
instead of being silently accepted. No `FederatedCallEntry` is created.
|
||||||
|
- **Path B** (no local DM): the zero-participant-match early return pushes to
|
||||||
|
`undeliverable` rather than `accepted`.
|
||||||
|
|
||||||
|
Other event types (messages, reactions, friend events, profile updates, etc.)
|
||||||
|
keep existing semantics — a message to an offline user is still `accepted`, since
|
||||||
|
messages persist and re-deliver on reconnect.
|
||||||
|
|
||||||
|
The field is optional on the wire. Old peers omit it; new peers include it only
|
||||||
|
when non-empty. Caller-side `sendCallRelay` parses the field (defaulting to an
|
||||||
|
empty array when missing), so upgrade skew is a no-op until both sides are on
|
||||||
|
new code.
|
||||||
|
|
||||||
### Inbound Relay Dispatch (`POST /api/federation/relay`)
|
### Inbound Relay Dispatch (`POST /api/federation/relay`)
|
||||||
|
|
||||||
Body limit: 10 MB. Max 50 events per batch. Rate-limited to 90 requests/min per peer (sliding window, keyed by `peer.origin`). Returns 429 when exceeded. Raised from 30 after FED-009 reduced the outbox worker interval from 10s to 1s — a busy sender can now hit 60 req/min during sustained traffic.
|
Body limit: 10 MB. Max 50 events per batch. Rate-limited to 90 requests/min per peer (sliding window, keyed by `peer.origin`). Returns 429 when exceeded. Raised from 30 after FED-009 reduced the outbox worker interval from 10s to 1s — a busy sender can now hit 60 req/min during sustained traffic.
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ All `dm_call_*` signaling events (`start`, `accept`, `reject`, `end`) are relaye
|
|||||||
| `reject` | false | Rejector's relay to host failed OR host's fan-out after a local reject failed; state already cleared. | No state change; info toast. |
|
| `reject` | false | Rejector's relay to host failed OR host's fan-out after a local reject failed; state already cleared. | No state change; info toast. |
|
||||||
| `end` | false | Ender's relay to host failed OR host's fan-out after a local end failed; state already cleared. | No state change; info toast. |
|
| `end` | false | Ender's relay to host failed OR host's fan-out after a local end failed; state already cleared. | No state change; info toast. |
|
||||||
| `host_unreachable` | true | A FederatedCallEntry's `federatedCallHost` peer transitions out of `active`, OR the 30s sentinel detects a non-active host for an existing entry. | Clear `activeDmCall` + `incomingCall`, disconnect LK, warning toast (*"Call ended — {label} became unreachable."*). |
|
| `host_unreachable` | true | A FederatedCallEntry's `federatedCallHost` peer transitions out of `active`, OR the 30s sentinel detects a non-active host for an existing entry. | Clear `activeDmCall` + `incomingCall`, disconnect LK, warning toast (*"Call ended — {label} became unreachable."*). |
|
||||||
|
| `no_recipient` | true | Remote returned 200 but had no reachable recipient (Path A: all members offline; Path B: zero participant matches). Caller fast-fails within the relay round-trip; ring room destroyed. | Clear `outgoingCall`, disconnect LK, warning toast (*"{peerLabel} couldn't ring anyone."*). Folds into multi-failure info copy when not the sole failure. |
|
||||||
|
|
||||||
**Accept-rollback semantics.** `handleDmCallAccept` Path 2 transitions the `FederatedCallEntry` to active and broadcasts `dm_call_accepted` optimistically so the acceptor's UI flips immediately. If the B→host relay fails, the server clears the entry, fans `dm_call_undeliverable { phase: 'accept', terminal: true }` out to all ringed users on B (via `sendToFederatedCallUsers`), and the client tears its call state back down.
|
**Accept-rollback semantics.** `handleDmCallAccept` Path 2 transitions the `FederatedCallEntry` to active and broadcasts `dm_call_accepted` optimistically so the acceptor's UI flips immediately. If the B→host relay fails, the server clears the entry, fans `dm_call_undeliverable { phase: 'accept', terminal: true }` out to all ringed users on B (via `sendToFederatedCallUsers`), and the client tears its call state back down.
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ reason: `'displaced'` (new tab) | `'session_closed'`
|
|||||||
| `dm_call_accepted` | dmChannelId?, federatedCallId? | DM members |
|
| `dm_call_accepted` | dmChannelId?, federatedCallId? | DM members |
|
||||||
| `dm_call_rejected` | dmChannelId?, federatedCallId? | DM members |
|
| `dm_call_rejected` | dmChannelId?, federatedCallId? | DM members |
|
||||||
| `dm_call_ended` | dmChannelId?, federatedCallId? | DM members |
|
| `dm_call_ended` | dmChannelId?, federatedCallId? | DM members |
|
||||||
| `dm_call_undeliverable` | Sent to the originator when a call relay (start / accept / reject / end) to one or more peers fails. Includes `phase: 'start' \| 'accept' \| 'reject' \| 'end'` identifying the action; `failures[]` enumerates failed peers with a `reason` (`peer_rejected` / `peer_awaiting_approval` / `peer_transient_failure` / `livekit_unavailable`). `terminal: true` means local call state should be (or has been) torn down; `terminal: false` is informational. See `docs/systems/voice.md` for the full phase × terminal matrix. | originator (caller / acceptor / rejector / ender) |
|
| `dm_call_undeliverable` | Sent to the originator when a call relay (start / accept / reject / end) to one or more peers fails. Includes `phase: 'start' \| 'accept' \| 'reject' \| 'end'` identifying the action; `failures[]` enumerates failed peers with a `reason` (`peer_rejected` / `peer_awaiting_approval` / `peer_transient_failure` / `livekit_unavailable` / `no_recipient`). `terminal: true` means local call state should be (or has been) torn down; `terminal: false` is informational. See `docs/systems/voice.md` for the full phase × terminal matrix. | originator (caller / acceptor / rejector / ender) |
|
||||||
|
|
||||||
### Social
|
### Social
|
||||||
| type | fields | scope |
|
| type | fields | scope |
|
||||||
|
|||||||
Reference in New Issue
Block a user