# macltok design decisions

the contract the working tree actually implements. if the code disagrees with
this, one of them is lying and it should be fixed

## two ways in, one way out

state changes are commands. `ClientHandle::send` queues one and hands back a
`Pending`, `execute` queues and waits, and there's an awaited convenience method
for every command. each command answers exactly once with
`Result<CommandOutput, Error>`. drop the `Pending` if you don't care, the
command still runs

pure reads skip the engine and ask the sdk directly through the handle. rooms,
members, power levels, account data, devices, push notification resolution,
and media bytes all live here

the event stream carries state and only state. timelines, history, typing,
rooms, connection, encryption, verification, upload progress, session. no
command results ride on it. drain it from its own task, it's bounded and the
engine waits for you

one error type for everything, `Error { kind, message, challenge }`. match on
`kind`. `Interrupted` means the engine stopped, the room closed, or the task
doing the work died before it could answer. `challenge` turns up with
`InteractiveAuthRequired`

## engine

the run loop routes and does nothing slow. every task it spawns is registered
with a context. a command task that panics costs its caller an `Interrupted`
and gets logged. a watcher or a room actor that panics takes the engine down,
emitting `Failed` then `Stopped`

sync restarts itself with backoff after an error or an unexpected stop, and
`ConnectionChanged` shows `Offline` meanwhile. every return to `Online` re-arms
the send queues, since the sdk parks a room's queue on any send failure and
never unparks it. a recoverable failure while online gets the room another go
after a few seconds

logout and a rejected token both stop the engine. `Shutdown` answers once
`Stopped` has gone out

## rooms

one actor per open room. the engine holds a slot per room, `Active` with a
sender or `Closing` with a fifo of opens and closes that arrived while it wound
down. those replay in order once the actor exits. a room you leave, or get
kicked or banned from, closes itself

opening sends `TimelineReset` on the stream before it answers. opening an
already open room sends a fresh reset, so a second frontend gets a snapshot

sends, edits and replies run on the actor in order, so they reach the send
queue in the order they were asked for. redactions, reactions and read
receipts are network calls, so they run beside the actor and a slow one
doesn't queue the room behind it. closing lets them finish rather than lie

threads belong to their room's actor. commands for a thread that's still
opening wait behind the open, up to the actor's queue capacity

pagination runs one request at a time per timeline and emits `HistoryChanged`
around each. the sdk remembers where history starts, we don't, because a gappy
sync can unload it again

every task inside an actor is supervised, and a panic in any of them panics the
actor

## timelines

`TimelineDiff::try_apply` refuses a diff whose indices don't fit your snapshot
and leaves the snapshot alone. stop applying, `resync`, and carry on from the
next `TimelineReset`. resync kills the old watcher before the new reset goes
out, so no stale diff can land after it

`TimelineId` tells a room from a thread by its root event

every item is an event, a day divider, the read marker or the start. an event
carries its sender with avatar and name ambiguity, and a `TimelineContent`
saying what it actually is. messages keep their msgtype, membership and profile
changes and the common room state changes get their own variants, and anything
else is `OtherState` or `Other` with its type. nothing is flattened into english
in a body field

replies carry the replied-to event once it's loaded. the watcher asks the sdk
for any it hasn't got, once per event, since the sdk won't on its own

edit, redact and react take an `EventRef`, so your own message can be edited or
reacted to before the server has echoed it back. redacting an unsent one just
cancels it

## mutations

membership, moderation, power levels, room creation and account data writes go
through one keyed fifo scheduler. same room, same alias, same dm partner or
same account data type run in order, everything else runs at once.
`create_direct` hands back the existing dm when there is one

notifications and encryption each get one serial worker. the encryption worker
owns the verification controller outright

power level changes build one state event from the local mapping, since the
sdk's own patcher is private

## media

media is a read. `ClientHandle::media` takes a `MediaId` a retained timeline
handed you, `mxc_media` takes any plain mxc uri. both share one bounded
download path with the byte cap the sdk's own download lacks, and
concurrent asks for the same bytes share one flight

the cache holds plaintext, keyed like the sdk keys it, so the sdk and macltok
can read each other's entries. that's how your own encrypted uploads come back

media ids are registered as timelines render and forgotten when their timeline
closes, least recently seen first once the cap is hit

uploads are commands that answer once the send queue has them. progress and
failure arrive as events from one watcher on the sdk's global send queue

## sessions and secrets

`Session::login_password`, `begin_oauth` and `restore` share one client builder
with encrypted sqlite, refresh tokens and threads. secret-bearing inputs don't
derive `Clone`

the caller owns the saved record. save `Session::record()` after login and
replace it atomically with `ClientHandle::session_record()` on every
`SessionTokensChanged`. logout and a hard rejection kill the record and the
store. a soft logout keeps the store for a same-device login, or the e2ee keys
go with it. a bare `Stopped` kills nothing. one store directory per login

## room lists, spaces and profiles

`RoomSummary` has what a room list row needs without a second query. topic,
alias, member counts, heroes for nameless rooms, the inviter, where an upgraded
room went, a latest event preview, the joined spaces listing it, and for a
space the children it lists

parents come from one lazily built `SpaceService` shared by every handle.
children come straight from the space's local state, joined or not.
`space_hierarchy` pages through the server's view for browsing rooms you
haven't joined, and holds its place between pages

`own_profile` and `user_profile` read profiles. name and avatar changes are
commands, and uploading an avatar gets the same byte cap as any upload

## notifications and room data

the notification client is built lazily and shares the engine's sync service.
sdk ui types stay behind the boundary

`rooms` normalises on demand with bounded concurrency and sorts by latest event,
then name, then id. `RoomsChanged` is an invalidation, coalesced so one sync
touching fifty rooms is one event

## verification

`Confirm`, `Reject` and `Cancel`. reject tells the other side the emoji didn't
match. the watcher and the action paths claim the terminal event, so a racing
cancel can't turn `Done` into `Cancelled`

## public model

owned ruma ids and typed data wherever there's a stable shape, `Raw` where
arbitrary content has to survive. anything expected to grow is
`#[non_exhaustive]`. `Engine::from_client` stays public for people who built
their own client
