# macltok request, pre-share room keys before send

## Problem

Sending into an encrypted room is slow in malice (and Element X), and under federation lag it can take over a minute. Clients on other SDKs feel instant because they do the key work before the user presses send.

matrix-sdk 0.18 does all of it inline on the send path. `SendRawMessageLikeEvent::into_future` (`matrix-sdk-0.18.0/src/room/futures.rs:190-245`) calls the private `ensure_room_encryption_ready` (`futures.rs:523-537`), which runs, in the room's single send-queue worker and before the `/send` PUT

1. `sync_members()` if `!are_members_synced()`
2. `query_keys_for_untracked_or_dirty_users()`, a `/keys/query` that the homeserver fans out over federation
3. `preshare_room_key()` (`room/mod.rs:2396-2431`), which claims one-time keys (`/keys/claim`, 10s server timeout, also federated) and then sends the megolm key to every device as to-device messages, awaiting each

Every request uses the default 30s timeout, and the send queue retries with `short_retry` (3 attempts, exponential backoff). A slow remote server stalls the send and everything queued behind it in that room.

For comparison, matrix-dart-sdk calls `prepareOutboundGroupSession` unawaited from `Client.setTyping`, so FluffyChat has usually finished steps 1 to 3 by the time the user hits send. matrix-js-sdk exposes `CryptoApi.prepareToEncrypt(room)` for the same purpose. matrix-sdk has the primitive but keeps it private, with this at `room/mod.rs:2392`

```rust
// TODO: expose this publicly so people can pre-share a group session if
// e.g. a user starts to type a message for a room.
```

## What is reachable today

Checked against the 0.18 sources. Only the first step is public.

| step | public from `matrix_sdk`? |
|---|---|
| `Room::sync_members`, `are_members_synced` | yes |
| `Room::query_keys_for_untracked_or_dirty_users` | no, private. `Encryption::request_user_identity(user_id)` does a per-user `/keys/query` |
| `Client::claim_one_time_keys` | no, `pub(crate)` |
| `Room::share_room_key`, `Room::preshare_room_key` | no, private. `OlmMachine::share_room_key` is pub but `Client::olm_machine()` is `pub(crate)` |
| `ensure_room_encryption_ready` | no, private free fn |

There is no public encrypt-without-send path either. Every caller of `ensure_room_encryption_ready` goes on to send.

## Request

Expose a command that runs the send path's encryption prep without sending anything, fire and forget, deduplicated per room.

```rust
impl ClientHandle {
    /// no-op for unencrypted rooms, coalesces with an in-flight prepare for the same room
    pub async fn prepare_to_send(&self, room_id: OwnedRoomId) -> Result<(), Error>;
}
```

malice would call it when the user starts typing in a room (alongside a typing notice) and probably when a room is opened. A later send then finds members synced, device lists fresh and the outbound session already shared, so only the `/send` PUT remains. Correctness is unchanged, since the send path still runs `ensure_room_encryption_ready` and redoes whatever went stale in between (a member joining after the prepare, say).

Suggested route, in order of preference

1. Upstream a small matrix-rust-sdk PR making `Room::preshare_room_key` public, or adding `pub async fn Room::prepare_encryption(&self)` that wraps `ensure_room_encryption_ready`. The TODO invites exactly this. Until it is released macltok could carry it via a `[patch.crates-io]` git fork, if that fits its dependency policy.
2. A partial warm-up using only public API while the PR is pending. `sync_members()` when not synced, then `request_user_identity` for members whose device lists are unknown, capped (members can be many and each query is a federated round trip). This removes step 1 and the `/keys/query` part of step 2 from the send path, which is the part federation lag hurts most. Steps 3 (`/keys/claim` and the to-device share) stay on the send path. It does not know which users are dirty (only `tracked_users()` is public), so it can over-query. Worth it only if option 1 will take a while.

malice will pair it with the existing `ClientHandle::set_typing(room_id, bool)` (`transport.rs:357`), so no new typing API is needed.

## Not requested

A "send without waiting for fresh device lists" mode. It would encrypt to stale device lists, so new members and new devices miss the key and see undecryptable messages. Pre-sharing gives the same latency without that.
