atagen

macltok

public
Star0
Watch0
960ac07210797dcdf9149ef8465c1ccd46dafbda
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: docs/design-decisions.md

96fdf90aacd6atagen2026-09-08 08:24 UTC1
# macltok design decisions
96fdf90aacd6atagen2026-09-08 08:24 UTC2
31ce2453c82fatagen2026-09-24 13:07 UTC3
the contract the working tree actually implements. if the code disagrees with
31ce2453c82fatagen2026-09-24 13:07 UTC4
this, one of them is lying and it should be fixed
31ce2453c82fatagen2026-09-24 13:07 UTC5
31ce2453c82fatagen2026-09-24 13:07 UTC6
## two ways in, one way out
31ce2453c82fatagen2026-09-24 13:07 UTC7
31ce2453c82fatagen2026-09-24 13:07 UTC8
state changes are commands. `ClientHandle::send` queues one and hands back a
31ce2453c82fatagen2026-09-24 13:07 UTC9
`Pending`, `execute` queues and waits, and there's an awaited convenience method
31ce2453c82fatagen2026-09-24 13:07 UTC10
for every command. each command answers exactly once with
31ce2453c82fatagen2026-09-24 13:07 UTC11
`Result<CommandOutput, Error>`. drop the `Pending` if you don't care, the
31ce2453c82fatagen2026-09-24 13:07 UTC12
command still runs
31ce2453c82fatagen2026-09-24 13:07 UTC13
31ce2453c82fatagen2026-09-24 13:07 UTC14
pure reads skip the engine and ask the sdk directly through the handle. rooms,
31ce2453c82fatagen2026-09-24 13:07 UTC15
members, power levels, account data, devices, push notification resolution,
31ce2453c82fatagen2026-09-24 13:07 UTC16
and media bytes all live here
31ce2453c82fatagen2026-09-24 13:07 UTC17
31ce2453c82fatagen2026-09-24 13:07 UTC18
the event stream carries state and only state. timelines, history, typing,
31ce2453c82fatagen2026-09-24 13:07 UTC19
rooms, connection, encryption, verification, upload progress, session. no
31ce2453c82fatagen2026-09-24 13:07 UTC20
command results ride on it. drain it from its own task, it's bounded and the
31ce2453c82fatagen2026-09-24 13:07 UTC21
engine waits for you
31ce2453c82fatagen2026-09-24 13:07 UTC22
31ce2453c82fatagen2026-09-24 13:07 UTC23
one error type for everything, `Error { kind, message, challenge }`. match on
31ce2453c82fatagen2026-09-24 13:07 UTC24
`kind`. `Interrupted` means the engine stopped, the room closed, or the task
31ce2453c82fatagen2026-09-24 13:07 UTC25
doing the work died before it could answer. `challenge` turns up with
31ce2453c82fatagen2026-09-24 13:07 UTC26
`InteractiveAuthRequired`
31ce2453c82fatagen2026-09-24 13:07 UTC27
31ce2453c82fatagen2026-09-24 13:07 UTC28
## engine
31ce2453c82fatagen2026-09-24 13:07 UTC29
31ce2453c82fatagen2026-09-24 13:07 UTC30
the run loop routes and does nothing slow. every task it spawns is registered
31ce2453c82fatagen2026-09-24 13:07 UTC31
with a context. a command task that panics costs its caller an `Interrupted`
31ce2453c82fatagen2026-09-24 13:07 UTC32
and gets logged. a watcher or a room actor that panics takes the engine down,
31ce2453c82fatagen2026-09-24 13:07 UTC33
emitting `Failed` then `Stopped`
31ce2453c82fatagen2026-09-24 13:07 UTC34
31ce2453c82fatagen2026-09-24 13:07 UTC35
sync restarts itself with backoff after an error or an unexpected stop, and
31ce2453c82fatagen2026-09-24 13:07 UTC36
`ConnectionChanged` shows `Offline` meanwhile. every return to `Online` re-arms
31ce2453c82fatagen2026-09-24 13:07 UTC37
the send queues, since the sdk parks a room's queue on any send failure and
31ce2453c82fatagen2026-09-24 13:07 UTC38
never unparks it. a recoverable failure while online gets the room another go
31ce2453c82fatagen2026-09-24 13:07 UTC39
after a few seconds
31ce2453c82fatagen2026-09-24 13:07 UTC40
31ce2453c82fatagen2026-09-24 13:07 UTC41
logout and a rejected token both stop the engine. `Shutdown` answers once
31ce2453c82fatagen2026-09-24 13:07 UTC42
`Stopped` has gone out
31ce2453c82fatagen2026-09-24 13:07 UTC43
31ce2453c82fatagen2026-09-24 13:07 UTC44
## rooms
31ce2453c82fatagen2026-09-24 13:07 UTC45
31ce2453c82fatagen2026-09-24 13:07 UTC46
one actor per open room. the engine holds a slot per room, `Active` with a
31ce2453c82fatagen2026-09-24 13:07 UTC47
sender or `Closing` with a fifo of opens and closes that arrived while it wound
31ce2453c82fatagen2026-09-24 13:07 UTC48
down. those replay in order once the actor exits. a room you leave, or get
31ce2453c82fatagen2026-09-24 13:07 UTC49
kicked or banned from, closes itself
31ce2453c82fatagen2026-09-24 13:07 UTC50
31ce2453c82fatagen2026-09-24 13:07 UTC51
opening sends `TimelineReset` on the stream before it answers. opening an
31ce2453c82fatagen2026-09-24 13:07 UTC52
already open room sends a fresh reset, so a second frontend gets a snapshot
31ce2453c82fatagen2026-09-24 13:07 UTC53
31ce2453c82fatagen2026-09-24 13:07 UTC54
sends, edits and replies run on the actor in order, so they reach the send
31ce2453c82fatagen2026-09-24 13:07 UTC55
queue in the order they were asked for. redactions, reactions and read
31ce2453c82fatagen2026-09-24 13:07 UTC56
receipts are network calls, so they run beside the actor and a slow one
31ce2453c82fatagen2026-09-24 13:07 UTC57
doesn't queue the room behind it. closing lets them finish rather than lie
31ce2453c82fatagen2026-09-24 13:07 UTC58
31ce2453c82fatagen2026-09-24 13:07 UTC59
threads belong to their room's actor. commands for a thread that's still
31ce2453c82fatagen2026-09-24 13:07 UTC60
opening wait behind the open, up to the actor's queue capacity
31ce2453c82fatagen2026-09-24 13:07 UTC61
31ce2453c82fatagen2026-09-24 13:07 UTC62
pagination runs one request at a time per timeline and emits `HistoryChanged`
31ce2453c82fatagen2026-09-24 13:07 UTC63
around each. the sdk remembers where history starts, we don't, because a gappy
31ce2453c82fatagen2026-09-24 13:07 UTC64
sync can unload it again
31ce2453c82fatagen2026-09-24 13:07 UTC65
31ce2453c82fatagen2026-09-24 13:07 UTC66
every task inside an actor is supervised, and a panic in any of them panics the
31ce2453c82fatagen2026-09-24 13:07 UTC67
actor
31ce2453c82fatagen2026-09-24 13:07 UTC68
31ce2453c82fatagen2026-09-24 13:07 UTC69
## timelines
31ce2453c82fatagen2026-09-24 13:07 UTC70
31ce2453c82fatagen2026-09-24 13:07 UTC71
`TimelineDiff::try_apply` refuses a diff whose indices don't fit your snapshot
31ce2453c82fatagen2026-09-24 13:07 UTC72
and leaves the snapshot alone. stop applying, `resync`, and carry on from the
31ce2453c82fatagen2026-09-24 13:07 UTC73
next `TimelineReset`. resync kills the old watcher before the new reset goes
31ce2453c82fatagen2026-09-24 13:07 UTC74
out, so no stale diff can land after it
31ce2453c82fatagen2026-09-24 13:07 UTC75
31ce2453c82fatagen2026-09-24 13:07 UTC76
`TimelineId` tells a room from a thread by its root event
31ce2453c82fatagen2026-09-24 13:07 UTC77
960ac0721079atagen2026-09-25 00:24 UTC78
every item is an event, a day divider, the read marker or the start. an event
960ac0721079atagen2026-09-25 00:24 UTC79
carries its sender with avatar and name ambiguity, and a `TimelineContent`
960ac0721079atagen2026-09-25 00:24 UTC80
saying what it actually is. messages keep their msgtype, membership and profile
960ac0721079atagen2026-09-25 00:24 UTC81
changes and the common room state changes get their own variants, and anything
960ac0721079atagen2026-09-25 00:24 UTC82
else is `OtherState` or `Other` with its type. nothing is flattened into english
960ac0721079atagen2026-09-25 00:24 UTC83
in a body field
960ac0721079atagen2026-09-25 00:24 UTC84
960ac0721079atagen2026-09-25 00:24 UTC85
replies carry the replied-to event once it's loaded. the watcher asks the sdk
960ac0721079atagen2026-09-25 00:24 UTC86
for any it hasn't got, once per event, since the sdk won't on its own
960ac0721079atagen2026-09-25 00:24 UTC87
960ac0721079atagen2026-09-25 00:24 UTC88
edit, redact and react take an `EventRef`, so your own message can be edited or
960ac0721079atagen2026-09-25 00:24 UTC89
reacted to before the server has echoed it back. redacting an unsent one just
960ac0721079atagen2026-09-25 00:24 UTC90
cancels it
960ac0721079atagen2026-09-25 00:24 UTC91
31ce2453c82fatagen2026-09-24 13:07 UTC92
## mutations
31ce2453c82fatagen2026-09-24 13:07 UTC93
31ce2453c82fatagen2026-09-24 13:07 UTC94
membership, moderation, power levels, room creation and account data writes go
31ce2453c82fatagen2026-09-24 13:07 UTC95
through one keyed fifo scheduler. same room, same alias, same dm partner or
31ce2453c82fatagen2026-09-24 13:07 UTC96
same account data type run in order, everything else runs at once.
31ce2453c82fatagen2026-09-24 13:07 UTC97
`create_direct` hands back the existing dm when there is one
31ce2453c82fatagen2026-09-24 13:07 UTC98
31ce2453c82fatagen2026-09-24 13:07 UTC99
notifications and encryption each get one serial worker. the encryption worker
31ce2453c82fatagen2026-09-24 13:07 UTC100
owns the verification controller outright
31ce2453c82fatagen2026-09-24 13:07 UTC101
31ce2453c82fatagen2026-09-24 13:07 UTC102
power level changes build one state event from the local mapping, since the
31ce2453c82fatagen2026-09-24 13:07 UTC103
sdk's own patcher is private
31ce2453c82fatagen2026-09-24 13:07 UTC104
31ce2453c82fatagen2026-09-24 13:07 UTC105
## media
31ce2453c82fatagen2026-09-24 13:07 UTC106
31ce2453c82fatagen2026-09-24 13:07 UTC107
media is a read. `ClientHandle::media` takes a `MediaId` a retained timeline
31ce2453c82fatagen2026-09-24 13:07 UTC108
handed you, `mxc_media` takes any plain mxc uri. both share one bounded
31ce2453c82fatagen2026-09-24 13:07 UTC109
download path with the byte cap the sdk's own download lacks, and
31ce2453c82fatagen2026-09-24 13:07 UTC110
concurrent asks for the same bytes share one flight
31ce2453c82fatagen2026-09-24 13:07 UTC111
31ce2453c82fatagen2026-09-24 13:07 UTC112
the cache holds plaintext, keyed like the sdk keys it, so the sdk and macltok
31ce2453c82fatagen2026-09-24 13:07 UTC113
can read each other's entries. that's how your own encrypted uploads come back
31ce2453c82fatagen2026-09-24 13:07 UTC114
31ce2453c82fatagen2026-09-24 13:07 UTC115
media ids are registered as timelines render and forgotten when their timeline
31ce2453c82fatagen2026-09-24 13:07 UTC116
closes, least recently seen first once the cap is hit
31ce2453c82fatagen2026-09-24 13:07 UTC117
31ce2453c82fatagen2026-09-24 13:07 UTC118
uploads are commands that answer once the send queue has them. progress and
31ce2453c82fatagen2026-09-24 13:07 UTC119
failure arrive as events from one watcher on the sdk's global send queue
31ce2453c82fatagen2026-09-24 13:07 UTC120
31ce2453c82fatagen2026-09-24 13:07 UTC121
## sessions and secrets
31ce2453c82fatagen2026-09-24 13:07 UTC122
31ce2453c82fatagen2026-09-24 13:07 UTC123
`Session::login_password`, `begin_oauth` and `restore` share one client builder
31ce2453c82fatagen2026-09-24 13:07 UTC124
with encrypted sqlite, refresh tokens and threads. secret-bearing inputs don't
31ce2453c82fatagen2026-09-24 13:07 UTC125
derive `Clone`
31ce2453c82fatagen2026-09-24 13:07 UTC126
31ce2453c82fatagen2026-09-24 13:07 UTC127
the caller owns the saved record. save `Session::record()` after login and
31ce2453c82fatagen2026-09-24 13:07 UTC128
replace it atomically with `ClientHandle::session_record()` on every
31ce2453c82fatagen2026-09-24 13:07 UTC129
`SessionTokensChanged`. logout and a hard rejection kill the record and the
31ce2453c82fatagen2026-09-24 13:07 UTC130
store. a soft logout keeps the store for a same-device login, or the e2ee keys
31ce2453c82fatagen2026-09-24 13:07 UTC131
go with it. a bare `Stopped` kills nothing. one store directory per login
31ce2453c82fatagen2026-09-24 13:07 UTC132
960ac0721079atagen2026-09-25 00:24 UTC133
## room lists, spaces and profiles
960ac0721079atagen2026-09-25 00:24 UTC134
960ac0721079atagen2026-09-25 00:24 UTC135
`RoomSummary` has what a room list row needs without a second query. topic,
960ac0721079atagen2026-09-25 00:24 UTC136
alias, member counts, heroes for nameless rooms, the inviter, where an upgraded
960ac0721079atagen2026-09-25 00:24 UTC137
room went, a latest event preview, the joined spaces listing it, and for a
960ac0721079atagen2026-09-25 00:24 UTC138
space the children it lists
960ac0721079atagen2026-09-25 00:24 UTC139
960ac0721079atagen2026-09-25 00:24 UTC140
parents come from one lazily built `SpaceService` shared by every handle.
960ac0721079atagen2026-09-25 00:24 UTC141
children come straight from the space's local state, joined or not.
960ac0721079atagen2026-09-25 00:24 UTC142
`space_hierarchy` pages through the server's view for browsing rooms you
960ac0721079atagen2026-09-25 00:24 UTC143
haven't joined, and holds its place between pages
960ac0721079atagen2026-09-25 00:24 UTC144
960ac0721079atagen2026-09-25 00:24 UTC145
`own_profile` and `user_profile` read profiles. name and avatar changes are
960ac0721079atagen2026-09-25 00:24 UTC146
commands, and uploading an avatar gets the same byte cap as any upload
960ac0721079atagen2026-09-25 00:24 UTC147
31ce2453c82fatagen2026-09-24 13:07 UTC148
## notifications and room data
31ce2453c82fatagen2026-09-24 13:07 UTC149
31ce2453c82fatagen2026-09-24 13:07 UTC150
the notification client is built lazily and shares the engine's sync service.
31ce2453c82fatagen2026-09-24 13:07 UTC151
sdk ui types stay behind the boundary
31ce2453c82fatagen2026-09-24 13:07 UTC152
31ce2453c82fatagen2026-09-24 13:07 UTC153
`rooms` normalises on demand with bounded concurrency and sorts by latest event,
31ce2453c82fatagen2026-09-24 13:07 UTC154
then name, then id. `RoomsChanged` is an invalidation, coalesced so one sync
31ce2453c82fatagen2026-09-24 13:07 UTC155
touching fifty rooms is one event
31ce2453c82fatagen2026-09-24 13:07 UTC156
31ce2453c82fatagen2026-09-24 13:07 UTC157
## verification
31ce2453c82fatagen2026-09-24 13:07 UTC158
31ce2453c82fatagen2026-09-24 13:07 UTC159
`Confirm`, `Reject` and `Cancel`. reject tells the other side the emoji didn't
31ce2453c82fatagen2026-09-24 13:07 UTC160
match. the watcher and the action paths claim the terminal event, so a racing
31ce2453c82fatagen2026-09-24 13:07 UTC161
cancel can't turn `Done` into `Cancelled`
31ce2453c82fatagen2026-09-24 13:07 UTC162
31ce2453c82fatagen2026-09-24 13:07 UTC163
## public model
31ce2453c82fatagen2026-09-24 13:07 UTC164
31ce2453c82fatagen2026-09-24 13:07 UTC165
owned ruma ids and typed data wherever there's a stable shape, `Raw` where
31ce2453c82fatagen2026-09-24 13:07 UTC166
arbitrary content has to survive. anything expected to grow is
31ce2453c82fatagen2026-09-24 13:07 UTC167
`#[non_exhaustive]`. `Engine::from_client` stays public for people who built
31ce2453c82fatagen2026-09-24 13:07 UTC168
their own client