atagen

macltok

public
Star0
Watch0
cb0668d30c14c14e0b1edddc9133ebf1e98bd5cb
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
9a25b6ce66f4atagen2026-10-08 11:15 UTC23
one error type for everything, `Error { kind, message, challenge, server }`.
9a25b6ce66f4atagen2026-10-08 11:15 UTC24
match on `kind`. `Interrupted` means the engine stopped, the room closed, or the
9a25b6ce66f4atagen2026-10-08 11:15 UTC25
task 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
9a25b6ce66f4atagen2026-10-08 11:15 UTC28
`message` is ours and says what we were doing. `server` is the homeserver's
9a25b6ce66f4atagen2026-10-08 11:15 UTC29
errcode and message, dug out of the source chain when the error is built, so a
9a25b6ce66f4atagen2026-10-08 11:15 UTC30
host can show "invalid password" without walking the chain. `AccountError`
9a25b6ce66f4atagen2026-10-08 11:15 UTC31
carries the same, plus a `kind`. neither folds it into `Display`, since anything
9a25b6ce66f4atagen2026-10-08 11:15 UTC32
printing the chain would say it twice
9a25b6ce66f4atagen2026-10-08 11:15 UTC33
31ce2453c82fatagen2026-09-24 13:07 UTC34
## engine
31ce2453c82fatagen2026-09-24 13:07 UTC35
31ce2453c82fatagen2026-09-24 13:07 UTC36
the run loop routes and does nothing slow. every task it spawns is registered
31ce2453c82fatagen2026-09-24 13:07 UTC37
with a context. a command task that panics costs its caller an `Interrupted`
31ce2453c82fatagen2026-09-24 13:07 UTC38
and gets logged. a watcher or a room actor that panics takes the engine down,
7b376d620a3datagen2026-09-26 12:53 UTC39
emitting `Failed` then `Stopped`. natively, anyway. on wasm a panic traps the
7b376d620a3datagen2026-09-26 12:53 UTC40
task mid poll and nothing is caught. the rest keeps running in an undefined
7b376d620a3datagen2026-09-26 12:53 UTC41
state and goes silent, no `Failed` and no `Stopped`, so the host hears of it
7b376d620a3datagen2026-09-26 12:53 UTC42
through `on_panic`, a free function since a panic hook belongs to the whole
7b376d620a3datagen2026-09-26 12:53 UTC43
instance rather than to one engine
31ce2453c82fatagen2026-09-24 13:07 UTC44
31ce2453c82fatagen2026-09-24 13:07 UTC45
sync restarts itself with backoff after an error or an unexpected stop, and
5a52012c9b1datagen2026-09-24 23:45 UTC46
`ConnectionChanged` shows `Offline` meanwhile. a run that drops again inside
5a52012c9b1datagen2026-09-24 23:45 UTC47
thirty seconds counts as a failure too, or a server that answers `/versions`
5a52012c9b1datagen2026-09-24 23:45 UTC48
but refuses sync would bounce us between online and offline forever. every return to `Online` re-arms
31ce2453c82fatagen2026-09-24 13:07 UTC49
the send queues, since the sdk parks a room's queue on any send failure and
31ce2453c82fatagen2026-09-24 13:07 UTC50
never unparks it. a recoverable failure while online gets the room another go
31ce2453c82fatagen2026-09-24 13:07 UTC51
after a few seconds
31ce2453c82fatagen2026-09-24 13:07 UTC52
fe4133dbcd47atagen2026-09-26 09:05 UTC53
logout and a rejected token both stop the engine. `run` resolves to a
fe4133dbcd47atagen2026-09-26 09:05 UTC54
`Termination` naming which of those it was, or a shutdown, or every handle
fe4133dbcd47atagen2026-09-26 09:05 UTC55
dropping, so the host never works it out from the events. `Shutdown` answers once
5a52012c9b1datagen2026-09-24 23:45 UTC56
`Stopped` has gone out, every one of them if several were queued
5a52012c9b1datagen2026-09-24 23:45 UTC57
5a52012c9b1datagen2026-09-24 23:45 UTC58
`Engine::run` hands back a boxed future. the real one nests deep enough that
5a52012c9b1datagen2026-09-24 23:45 UTC59
every crate spawning it would trip the compiler's query depth
31ce2453c82fatagen2026-09-24 13:07 UTC60
31ce2453c82fatagen2026-09-24 13:07 UTC61
## rooms
31ce2453c82fatagen2026-09-24 13:07 UTC62
31ce2453c82fatagen2026-09-24 13:07 UTC63
one actor per open room. the engine holds a slot per room, `Active` with a
31ce2453c82fatagen2026-09-24 13:07 UTC64
sender or `Closing` with a fifo of opens and closes that arrived while it wound
31ce2453c82fatagen2026-09-24 13:07 UTC65
down. those replay in order once the actor exits. a room you leave, or get
5a52012c9b1datagen2026-09-24 23:45 UTC66
kicked or banned from, closes itself. it tells the engine first, and anything
5a52012c9b1datagen2026-09-24 23:45 UTC67
already queued to it comes back to be answered or replayed, so an open racing a
5a52012c9b1datagen2026-09-24 23:45 UTC68
leave still gets a fresh actor
31ce2453c82fatagen2026-09-24 13:07 UTC69
0bfa3f46f0beatagen2026-09-26 09:12 UTC70
however an actor ends, short of the engine stopping, its typing goes out empty
0bfa3f46f0beatagen2026-09-26 09:12 UTC71
and each of its timelines sends `TimelineClosed`. so does a closed thread. a
0bfa3f46f0beatagen2026-09-26 09:12 UTC72
timeline that never sent a reset never sends a close either
0bfa3f46f0beatagen2026-09-26 09:12 UTC73
31ce2453c82fatagen2026-09-24 13:07 UTC74
opening sends `TimelineReset` on the stream before it answers. opening an
31ce2453c82fatagen2026-09-24 13:07 UTC75
already open room sends a fresh reset, so a second frontend gets a snapshot
31ce2453c82fatagen2026-09-24 13:07 UTC76
31ce2453c82fatagen2026-09-24 13:07 UTC77
sends, edits and replies run on the actor in order, so they reach the send
31ce2453c82fatagen2026-09-24 13:07 UTC78
queue in the order they were asked for. redactions, reactions and read
31ce2453c82fatagen2026-09-24 13:07 UTC79
receipts are network calls, so they run beside the actor and a slow one
31ce2453c82fatagen2026-09-24 13:07 UTC80
doesn't queue the room behind it. closing lets them finish rather than lie
31ce2453c82fatagen2026-09-24 13:07 UTC81
31ce2453c82fatagen2026-09-24 13:07 UTC82
threads belong to their room's actor. commands for a thread that's still
31ce2453c82fatagen2026-09-24 13:07 UTC83
opening wait behind the open, up to the actor's queue capacity
31ce2453c82fatagen2026-09-24 13:07 UTC84
31ce2453c82fatagen2026-09-24 13:07 UTC85
pagination runs one request at a time per timeline and emits `HistoryChanged`
31ce2453c82fatagen2026-09-24 13:07 UTC86
around each. the sdk remembers where history starts, we don't, because a gappy
31ce2453c82fatagen2026-09-24 13:07 UTC87
sync can unload it again
31ce2453c82fatagen2026-09-24 13:07 UTC88
5a52012c9b1datagen2026-09-24 23:45 UTC89
the actor's watchers are supervised, and a panic in any of them panics the
5a52012c9b1datagen2026-09-24 23:45 UTC90
actor. its command work isn't, a panicking redaction just answers `Interrupted`
5a52012c9b1datagen2026-09-24 23:45 UTC91
and gets logged
31ce2453c82fatagen2026-09-24 13:07 UTC92
31ce2453c82fatagen2026-09-24 13:07 UTC93
## timelines
31ce2453c82fatagen2026-09-24 13:07 UTC94
31ce2453c82fatagen2026-09-24 13:07 UTC95
`TimelineDiff::try_apply` refuses a diff whose indices don't fit your snapshot
d5d8097c3afdatagen2026-09-26 09:33 UTC96
and leaves the snapshot alone. `try_apply_all` does the same for a whole batch,
d5d8097c3afdatagen2026-09-26 09:33 UTC97
checking every diff against lengths alone first, since that's all a diff's fit
d5d8097c3afdatagen2026-09-26 09:33 UTC98
depends on. snapshots are `imbl::Vector`s so keeping one is a pointer copy.
d5d8097c3afdatagen2026-09-26 09:33 UTC99
stop applying, `resync`, and carry on from the next `TimelineReset`. resync
d5d8097c3afdatagen2026-09-26 09:33 UTC100
kills the old watcher before the new reset goes out, so no stale diff can land
d5d8097c3afdatagen2026-09-26 09:33 UTC101
after it
31ce2453c82fatagen2026-09-24 13:07 UTC102
31ce2453c82fatagen2026-09-24 13:07 UTC103
`TimelineId` tells a room from a thread by its root event
31ce2453c82fatagen2026-09-24 13:07 UTC104
960ac0721079atagen2026-09-25 00:24 UTC105
every item is an event, a day divider, the read marker or the start. an event
960ac0721079atagen2026-09-25 00:24 UTC106
carries its sender with avatar and name ambiguity, and a `TimelineContent`
960ac0721079atagen2026-09-25 00:24 UTC107
saying what it actually is. messages keep their msgtype, membership and profile
960ac0721079atagen2026-09-25 00:24 UTC108
changes and the common room state changes get their own variants, and anything
960ac0721079atagen2026-09-25 00:24 UTC109
else is `OtherState` or `Other` with its type. nothing is flattened into english
960ac0721079atagen2026-09-25 00:24 UTC110
in a body field
960ac0721079atagen2026-09-25 00:24 UTC111
5a52012c9b1datagen2026-09-24 23:45 UTC112
thread receipts for a thread that isn't open go straight to the room, so
5a52012c9b1datagen2026-09-24 23:45 UTC113
marking a thread read works the same whether its room is on screen or not
5a52012c9b1datagen2026-09-24 23:45 UTC114
960ac0721079atagen2026-09-25 00:24 UTC115
replies carry the replied-to event once it's loaded. the watcher asks the sdk
960ac0721079atagen2026-09-25 00:24 UTC116
for any it hasn't got, once per event, since the sdk won't on its own
960ac0721079atagen2026-09-25 00:24 UTC117
960ac0721079atagen2026-09-25 00:24 UTC118
edit, redact and react take an `EventRef`, so your own message can be edited or
960ac0721079atagen2026-09-25 00:24 UTC119
reacted to before the server has echoed it back. redacting an unsent one just
960ac0721079atagen2026-09-25 00:24 UTC120
cancels it
960ac0721079atagen2026-09-25 00:24 UTC121
38c4caecdb3datagen2026-09-26 13:35 UTC122
## warming up to send
38c4caecdb3datagen2026-09-26 13:35 UTC123
38c4caecdb3datagen2026-09-26 13:35 UTC124
the sdk does all its key work on the send path, in the room's send queue before
38c4caecdb3datagen2026-09-26 13:35 UTC125
the `/send` itself. members synced, a `/keys/query` for anyone untracked or
38c4caecdb3datagen2026-09-26 13:35 UTC126
dirty, then a `/keys/claim` and the room key sent to every device. over a slow
38c4caecdb3datagen2026-09-26 13:35 UTC127
federation that's a minute of nothing. `prepare_to_send` does what the public
38c4caecdb3datagen2026-09-26 13:35 UTC128
api lets us of that, ahead of time, which comes down to syncing the members if
38c4caecdb3datagen2026-09-26 13:35 UTC129
they aren't. the sdk dedups that with its own sync, and it marks every member
38c4caecdb3datagen2026-09-26 13:35 UTC130
tracked and dirty, so the sdk's background outgoing requests run the
38c4caecdb3datagen2026-09-26 13:35 UTC131
`/keys/query` long before the send would have. that's what moves the query off
38c4caecdb3datagen2026-09-26 13:35 UTC132
the send path. we don't ask `request_user_identity` for anyone, since that's
38c4caecdb3datagen2026-09-26 13:35 UTC133
an out of band query that never marks a user tracked or up to date
38c4caecdb3datagen2026-09-26 13:35 UTC134
(matrix-sdk-crypto `identities/manager.rs` ~804-817), federated traffic the
38c4caecdb3datagen2026-09-26 13:35 UTC135
send path would redo anyway. a synced room costs nothing, so there's no
38c4caecdb3datagen2026-09-26 13:35 UTC136
cooldown. one warm-up per room at a time, a second ask joins it. opening a room
38c4caecdb3datagen2026-09-26 13:35 UTC137
or starting to type runs one too, unless `EngineConfig::prepare_to_send` is
38c4caecdb3datagen2026-09-26 13:35 UTC138
off. it's on because malice asked, not because anyone decided
38c4caecdb3datagen2026-09-26 13:35 UTC139
38c4caecdb3datagen2026-09-26 13:35 UTC140
that's the leftovers of turning down a matrix-sdk fork. the `/keys/claim` and
38c4caecdb3datagen2026-09-26 13:35 UTC141
the room key share, the slow federated half, stay on the send path. the
38c4caecdb3datagen2026-09-26 13:35 UTC142
background `/keys/query` runs when the sdk next sends its outgoing requests,
38c4caecdb3datagen2026-09-26 13:35 UTC143
after a sync response, and `send_outgoing_requests` is crate private, so a send
38c4caecdb3datagen2026-09-26 13:35 UTC144
straight after a warm-up can still find the members dirty and query them itself.
38c4caecdb3datagen2026-09-26 13:35 UTC145
and we can't join the dedup the sdk wraps its own prep in
38c4caecdb3datagen2026-09-26 13:35 UTC146
38c4caecdb3datagen2026-09-26 13:35 UTC147
the hook upstream is the todo above `preshare_room_key` at matrix-sdk
38c4caecdb3datagen2026-09-26 13:35 UTC148
`room/mod.rs` ~2392, asking for exactly this. it's still private on main and in
38c4caecdb3datagen2026-09-26 13:35 UTC149
0.19.1 as of 2026-09-26. once upstream makes it public, or a fork becomes
38c4caecdb3datagen2026-09-26 13:35 UTC150
acceptable, `prepare_to_send` should just call it and most of this goes
38c4caecdb3datagen2026-09-26 13:35 UTC151
31ce2453c82fatagen2026-09-24 13:07 UTC152
## mutations
31ce2453c82fatagen2026-09-24 13:07 UTC153
31ce2453c82fatagen2026-09-24 13:07 UTC154
membership, moderation, power levels, room creation and account data writes go
31ce2453c82fatagen2026-09-24 13:07 UTC155
through one keyed fifo scheduler. same room, same alias, same dm partner or
31ce2453c82fatagen2026-09-24 13:07 UTC156
same account data type run in order, everything else runs at once.
31ce2453c82fatagen2026-09-24 13:07 UTC157
`create_direct` hands back the existing dm when there is one
31ce2453c82fatagen2026-09-24 13:07 UTC158
31ce2453c82fatagen2026-09-24 13:07 UTC159
notifications and encryption each get one serial worker. the encryption worker
31ce2453c82fatagen2026-09-24 13:07 UTC160
owns the verification controller outright
31ce2453c82fatagen2026-09-24 13:07 UTC161
31ce2453c82fatagen2026-09-24 13:07 UTC162
power level changes build one state event from the local mapping, since the
31ce2453c82fatagen2026-09-24 13:07 UTC163
sdk's own patcher is private
31ce2453c82fatagen2026-09-24 13:07 UTC164
31ce2453c82fatagen2026-09-24 13:07 UTC165
## media
31ce2453c82fatagen2026-09-24 13:07 UTC166
5d2974de5826atagen2026-10-08 11:29 UTC167
media is a read. `ClientHandle::media` takes a `MediaId`, from a retained
5d2974de5826atagen2026-10-08 11:29 UTC168
timeline or an avatar, through one bounded
5a52012c9b1datagen2026-09-24 23:45 UTC169
download path with the byte cap the sdk's own download lacks, an hour's total
5a52012c9b1datagen2026-09-24 23:45 UTC170
deadline instead of the sdk client's thirty seconds, and a minute's patience
5a52012c9b1datagen2026-09-24 23:45 UTC171
for a stalled stream. concurrent asks for the same bytes share one flight.
5a52012c9b1datagen2026-09-24 23:45 UTC172
a thumbnail the server won't make falls back to the file itself.
5a52012c9b1datagen2026-09-24 23:45 UTC173
`MediaId` parses from a string, since a web client has to round trip it
31ce2453c82fatagen2026-09-24 13:07 UTC174
31ce2453c82fatagen2026-09-24 13:07 UTC175
the cache holds plaintext, keyed like the sdk keys it, so the sdk and macltok
5a52012c9b1datagen2026-09-24 23:45 UTC176
can read each other's entries. that's how your own encrypted uploads come back.
5a52012c9b1datagen2026-09-24 23:45 UTC177
the sdk keys plain and encrypted sources alike, so a plain event pointing at
5a52012c9b1datagen2026-09-24 23:45 UTC178
someone's encrypted file can leave ciphertext in the cache. an encrypted read
5a52012c9b1datagen2026-09-24 23:45 UTC179
checks the cached bytes against the file's hash and decrypts them if they match
31ce2453c82fatagen2026-09-24 13:07 UTC180
31ce2453c82fatagen2026-09-24 13:07 UTC181
media ids are registered as timelines render and forgotten when their timeline
31ce2453c82fatagen2026-09-24 13:07 UTC182
closes, least recently seen first once the cap is hit
31ce2453c82fatagen2026-09-24 13:07 UTC183
5d2974de5826atagen2026-10-08 11:29 UTC184
avatars are `MediaId`s too, so a host never handles an mxc uri. they aren't
5d2974de5826atagen2026-10-08 11:29 UTC185
registered, since they live in the room list and member lists for the whole
5d2974de5826atagen2026-10-08 11:29 UTC186
session and there can be thousands, and the cap would evict ones still on
5d2974de5826atagen2026-10-08 11:29 UTC187
screen. a plain avatar carries no keys, so its id is its mxc uri and resolves
5d2974de5826atagen2026-10-08 11:29 UTC188
without the registry. it never expires, and an mxc uri a host got some other way
5d2974de5826atagen2026-10-08 11:29 UTC189
parses into one. `set_avatar` still takes an mxc uri
5d2974de5826atagen2026-10-08 11:29 UTC190
5b21b919d1e6atagen2026-09-27 11:47 UTC191
streaming decryption inside macltok was declined. in a browser the service
5b21b919d1e6atagen2026-09-27 11:47 UTC192
worker answering range requests is a separate context from the worker running
5b21b919d1e6atagen2026-09-27 11:47 UTC193
macltok, and streaming through macltok would carry every chunk through wasm
5b21b919d1e6atagen2026-09-27 11:47 UTC194
memory, the very thing the host streams to avoid. `media_source` hands over the
5b21b919d1e6atagen2026-09-27 11:47 UTC195
url and keys `media` would have used, picked the same way, and `access_token`
5b21b919d1e6atagen2026-09-27 11:47 UTC196
the rest. the byte cap doesn't apply, nothing is buffered
5b21b919d1e6atagen2026-09-27 11:47 UTC197
5a52012c9b1datagen2026-09-24 23:45 UTC198
uploads are commands that answer once the send queue has them. progress only
5a52012c9b1datagen2026-09-24 23:45 UTC199
goes out on a room's own send queue channel, so each room that uploads gets a
5a52012c9b1datagen2026-09-24 23:45 UTC200
forwarder, subscribed before the upload is queued and restarted for uploads the
0bfa3f46f0beatagen2026-09-26 09:12 UTC201
sdk resumes from last time. everything but progress reaches the client-wide
0bfa3f46f0beatagen2026-09-26 09:12 UTC202
channel, so one watcher there follows every attachment in the queue and ends
0bfa3f46f0beatagen2026-09-26 09:12 UTC203
each with one `MediaUploadFinished`. sent, cancelled, its room forgotten, or
0bfa3f46f0beatagen2026-09-26 09:12 UTC204
missed when that channel overflowed at the wrong moment, since then there's no
0bfa3f46f0beatagen2026-09-26 09:12 UTC205
knowing which. a failure isn't an ending, a wedged upload can still be retried.
0bfa3f46f0beatagen2026-09-26 09:12 UTC206
uploads don't belong to a room's actor, so closing the room leaves them be
31ce2453c82fatagen2026-09-24 13:07 UTC207
7c26fad34822atagen2026-09-26 09:41 UTC208
the forwarders hand their progress to that watcher rather than the stream, so
7c26fad34822atagen2026-09-26 09:41 UTC209
it's the only thing speaking about uploads. anything that ever showed progress
7c26fad34822atagen2026-09-26 09:41 UTC210
or a failure is followed until it gets exactly one `MediaUploadFinished`, and
7cf348521204atagen2026-09-26 12:28 UTC211
nothing about it goes out after. each forwarder relays its room's sends and
7cf348521204atagen2026-09-26 12:28 UTC212
cancels behind its progress, so an end never overtakes the ticks before it. an
7cf348521204atagen2026-09-26 12:28 UTC213
upload ended from anywhere else is remembered until its forwarder's own end
7cf348521204atagen2026-09-26 12:28 UTC214
turns up, since until then there may be progress still queued for it. the
7cf348521204atagen2026-09-26 12:28 UTC215
engine stopping ends nothing, `Stopped` covers it
7c26fad34822atagen2026-09-26 09:41 UTC216
31ce2453c82fatagen2026-09-24 13:07 UTC217
## sessions and secrets
31ce2453c82fatagen2026-09-24 13:07 UTC218
31ce2453c82fatagen2026-09-24 13:07 UTC219
`Session::login_password`, `begin_oauth` and `restore` share one client builder
62fe32759529atagen2026-09-26 08:50 UTC220
with an encrypted store, refresh tokens and threads. the store is sqlite
62fe32759529atagen2026-09-26 08:50 UTC221
natively and indexeddb in a browser. secret-bearing inputs don't derive `Clone`
31ce2453c82fatagen2026-09-24 13:07 UTC222
9c05b1dd327aatagen2026-10-08 11:27 UTC223
the homeserver a person typed is checked before the client sees it, and a bad
9c05b1dd327aatagen2026-10-08 11:27 UTC224
one is `AccountError::InvalidHomeserver`. a bare server name passes untouched,
9c05b1dd327aatagen2026-10-08 11:27 UTC225
since that's what sends the sdk to `.well-known`, and a host that "helpfully"
9c05b1dd327aatagen2026-10-08 11:27 UTC226
adds `https://` turns `example.org` into a homeserver url and skips discovery.
9c05b1dd327aatagen2026-10-08 11:27 UTC227
a url needs a host, no userinfo, and https. plain http needs
9c05b1dd327aatagen2026-10-08 11:27 UTC228
`allow_insecure_loopback` and a loopback host. `SoftLogout::password_login` sets
9c05b1dd327aatagen2026-10-08 11:27 UTC229
it, since its homeserver passed once already
9c05b1dd327aatagen2026-10-08 11:27 UTC230
713205a95af2atagen2026-09-26 09:24 UTC231
a `SessionStore` owns one login's record and its store. login saves the first
f12377bce9b9atagen2026-09-26 12:32 UTC232
record, `restore` loads it, and the engine a `Session` builds saves every token
f12377bce9b9atagen2026-09-26 12:32 UTC233
rotation after that, since a refresh token works once and a host that missed one
f12377bce9b9atagen2026-09-26 12:32 UTC234
save has lost the login. the session changes are subscribed the moment the
f12377bce9b9atagen2026-09-26 12:32 UTC235
client exists, because the sdk can refresh during restore before any engine
f12377bce9b9atagen2026-09-26 12:32 UTC236
does. a save runs off the event stream, so a full stream never holds it back,
f12377bce9b9atagen2026-09-26 12:32 UTC237
and `SessionTokensChanged` only goes out once it has landed, or
f12377bce9b9atagen2026-09-26 12:32 UTC238
`SessionSaveFailed` if it didn't. natively the sdk's save callback runs inside
f12377bce9b9atagen2026-09-26 12:32 UTC239
the refresh, before the new token is used, and `save_blocking` writes there and
f12377bce9b9atagen2026-09-26 12:32 UTC240
then. in a browser there's only the async save, so a tab that dies between a
f12377bce9b9atagen2026-09-26 12:32 UTC241
refresh and its save wakes up holding a spent token. `without_persistence`
f12377bce9b9atagen2026-09-26 12:32 UTC242
hands the job back to the host. `EncryptedSessionStore` seals the record with
713205a95af2atagen2026-09-26 09:24 UTC243
the sdk's `StoreCipher` under the store's passphrase and replaces it by rename,
713205a95af2atagen2026-09-26 09:24 UTC244
so a crash mid save leaves the old one. it keeps the record out of the sdk's
713205a95af2atagen2026-09-26 09:24 UTC245
state store because sqlite writes custom values there in the clear, and a
713205a95af2atagen2026-09-26 09:24 UTC246
client can't open its store before it knows the homeserver the record holds.
713205a95af2atagen2026-09-26 09:24 UTC247
`discard` deletes the record, then the store, since a record without its store
6f1d6591c10datagen2026-09-26 09:52 UTC248
would restore into empty crypto. the host follows `Termination::retain`.
6f1d6591c10datagen2026-09-26 09:52 UTC249
`Nothing` discards. `Store` calls `soft_log_out`, which keeps the store and
6f1d6591c10datagen2026-09-26 09:52 UTC250
swaps the record for one holding the homeserver, user, device and oauth client
6f1d6591c10datagen2026-09-26 09:52 UTC251
id with no tokens. `restore` hands that back as `Restore::SoftLoggedOut`, so a
6f1d6591c10datagen2026-09-26 09:52 UTC252
restart still knows which device to log in again as, and the login overwrites
a250c7fa43fbatagen2026-09-26 13:35 UTC253
it. one store directory per login. a login whose first save fails comes back as
a250c7fa43fbatagen2026-09-26 13:35 UTC254
`AccountError::Unsaved` holding the live session, so the host retries the save
a250c7fa43fbatagen2026-09-26 13:35 UTC255
rather than spending another device
a250c7fa43fbatagen2026-09-26 13:35 UTC256
cb0668d30c14atagen2026-10-08 11:34 UTC257
`Accounts` picks the store, so a host never decides which account a login form
cb0668d30c14atagen2026-10-08 11:34 UTC258
means. it's one owner's accounts under one root and one passphrase, a store and
cb0668d30c14atagen2026-10-08 11:34 UTC259
record each, named by a random id. a password login that names a
cb0668d30c14atagen2026-10-08 11:34 UTC260
soft-logged-out account, by localpart or full user id on the same server, logs
cb0668d30c14atagen2026-10-08 11:34 UTC261
back in as that device into that store. the server is compared by name as well
cb0668d30c14atagen2026-10-08 11:34 UTC262
as by url, since a typed `example.org` is stored as wherever discovery led. a
cb0668d30c14atagen2026-10-08 11:34 UTC263
live account it names is `AlreadyLoggedIn`, anyone else gets a new store. oauth
cb0668d30c14atagen2026-10-08 11:34 UTC264
can't know who it's for until it finishes, so it resumes only the device
cb0668d30c14atagen2026-10-08 11:34 UTC265
`SoftLogout::oauth_login` names. a new store is locked before it's listed, so
cb0668d30c14atagen2026-10-08 11:34 UTC266
`list` can sweep stores with no record, left by a login that died or a discard
cb0668d30c14atagen2026-10-08 11:34 UTC267
that didn't finish, without catching one mid login. a browser lists accounts in
cb0668d30c14atagen2026-10-08 11:34 UTC268
an IndexedDB database of their own, a key each, so two tabs adding at once
cb0668d30c14atagen2026-10-08 11:34 UTC269
don't lose one
cb0668d30c14atagen2026-10-08 11:34 UTC270
cb0668d30c14atagen2026-10-08 11:34 UTC271
the set is per owner because resuming needs only the password, and the store
cb0668d30c14atagen2026-10-08 11:34 UTC272
holds the device's e2ee keys. a set shared across people would hand someone who
cb0668d30c14atagen2026-10-08 11:34 UTC273
knows a password the history a fresh login wouldn't get
cb0668d30c14atagen2026-10-08 11:34 UTC274
a250c7fa43fbatagen2026-09-26 13:35 UTC275
one engine per store. before the client opens the store we take the platform's
a250c7fa43fbatagen2026-09-26 13:35 UTC276
own exclusive lock on it, `File::try_lock` on `macltok.lock` in the store
a250c7fa43fbatagen2026-09-26 13:35 UTC277
directory natively and a Web Lock named after the store in a browser. the
a250c7fa43fbatagen2026-09-26 13:35 UTC278
engine and every `ClientHandle` share it, so it goes when the last of them
a250c7fa43fbatagen2026-09-26 13:35 UTC279
does, since a lingering handle still has the client and its store open. anyone
a250c7fa43fbatagen2026-09-26 13:35 UTC280
else, in this process, another one or another tab, gets `StoreInUse` at once,
a250c7fa43fbatagen2026-09-26 13:35 UTC281
so the host decides whether to wait, hand over or say so. the os or the browser
a250c7fa43fbatagen2026-09-26 13:35 UTC282
lets go when the holder dies. a SharedWorker serving every tab from one engine
a250c7fa43fbatagen2026-09-26 13:35 UTC283
never contends for it. Web Locks only exist in a secure context, so a browser
a250c7fa43fbatagen2026-09-26 13:35 UTC284
store needs one too
a250c7fa43fbatagen2026-09-26 13:35 UTC285
a250c7fa43fbatagen2026-09-26 13:35 UTC286
the sdk's cross process lease can't do this job. in matrix-sdk-common 0.18
a250c7fa43fbatagen2026-09-26 13:35 UTC287
`CrossProcessLock::try_lock_once` (`cross_process_lock.rs` 389-448) leases for
a250c7fa43fbatagen2026-09-26 13:35 UTC288
500ms and renews every 50ms from a spawned task, which gives up for good on one
a250c7fa43fbatagen2026-09-26 13:35 UTC289
failed renewal or on finding the lease lapsed and retaken, and tells no one. a
a250c7fa43fbatagen2026-09-26 13:35 UTC290
stall past half a second, ordinary in a tab and certain during the record's
a250c7fa43fbatagen2026-09-26 13:35 UTC291
PBKDF2, or one `SQLITE_BUSY`, hands the store to a second engine while the
a250c7fa43fbatagen2026-09-26 13:35 UTC292
first still thinks it's alone. with our lock keeping everyone else out, clients
a250c7fa43fbatagen2026-09-26 13:35 UTC293
run with `CrossProcessLockConfig::SingleProcess` and the sdk's own sections skip
a250c7fa43fbatagen2026-09-26 13:35 UTC294
the lease writes and renewal tasks
a250c7fa43fbatagen2026-09-26 13:35 UTC295
a250c7fa43fbatagen2026-09-26 13:35 UTC296
a pending OAuth login isn't persisted. `begin_oauth` and `finish` have to run in
a250c7fa43fbatagen2026-09-26 13:35 UTC297
the same process, so a web host runs the login in a popup or in a worker that
a250c7fa43fbatagen2026-09-26 13:35 UTC298
outlives the redirect. the sdk keeps what `finish_login` needs, the PKCE
a250c7fa43fbatagen2026-09-26 13:35 UTC299
verifier among it, in `AuthorizationValidationData` inside a `pub(crate)` map
a250c7fa43fbatagen2026-09-26 13:35 UTC300
keyed by `CsrfToken` (matrix-sdk 0.18, `authentication/oauth/mod.rs` around
a250c7fa43fbatagen2026-09-26 13:35 UTC301
262-266 and 1744-1756). nothing public reads it or puts it back, and
a250c7fa43fbatagen2026-09-26 13:35 UTC302
`finish_login` on a fresh client answers `InvalidState`. doing the code grant
a250c7fa43fbatagen2026-09-26 13:35 UTC303
ourselves would copy some 250 lines of the sdk's private oauth behaviour and
a250c7fa43fbatagen2026-09-26 13:35 UTC304
drift from it every upgrade. patching the sdk to export and import that data is
a250c7fa43fbatagen2026-09-26 13:35 UTC305
small, but it means carrying a matrix-sdk fork, which we won't for now. upstream
a250c7fa43fbatagen2026-09-26 13:35 UTC306
exposing export and import of pending authorization data, or a fork becoming
a250c7fa43fbatagen2026-09-26 13:35 UTC307
acceptable, reopens this
31ce2453c82fatagen2026-09-24 13:07 UTC308
960ac0721079atagen2026-09-25 00:24 UTC309
## room lists, spaces and profiles
960ac0721079atagen2026-09-25 00:24 UTC310
960ac0721079atagen2026-09-25 00:24 UTC311
`RoomSummary` has what a room list row needs without a second query. topic,
960ac0721079atagen2026-09-25 00:24 UTC312
alias, member counts, heroes for nameless rooms, the inviter, where an upgraded
960ac0721079atagen2026-09-25 00:24 UTC313
room went, a latest event preview, the joined spaces listing it, and for a
960ac0721079atagen2026-09-25 00:24 UTC314
space the children it lists
960ac0721079atagen2026-09-25 00:24 UTC315
960ac0721079atagen2026-09-25 00:24 UTC316
parents come from one lazily built `SpaceService` shared by every handle.
960ac0721079atagen2026-09-25 00:24 UTC317
children come straight from the space's local state, joined or not.
960ac0721079atagen2026-09-25 00:24 UTC318
`space_hierarchy` pages through the server's view for browsing rooms you
960ac0721079atagen2026-09-25 00:24 UTC319
haven't joined, and holds its place between pages
960ac0721079atagen2026-09-25 00:24 UTC320
960ac0721079atagen2026-09-25 00:24 UTC321
`own_profile` and `user_profile` read profiles. name and avatar changes are
960ac0721079atagen2026-09-25 00:24 UTC322
commands, and uploading an avatar gets the same byte cap as any upload
960ac0721079atagen2026-09-25 00:24 UTC323
31ce2453c82fatagen2026-09-24 13:07 UTC324
## notifications and room data
31ce2453c82fatagen2026-09-24 13:07 UTC325
31ce2453c82fatagen2026-09-24 13:07 UTC326
the notification client is built lazily and shares the engine's sync service.
31ce2453c82fatagen2026-09-24 13:07 UTC327
sdk ui types stay behind the boundary
31ce2453c82fatagen2026-09-24 13:07 UTC328
31ce2453c82fatagen2026-09-24 13:07 UTC329
`rooms` normalises on demand with bounded concurrency and sorts by latest event,
31ce2453c82fatagen2026-09-24 13:07 UTC330
then name, then id. `RoomsChanged` is an invalidation, coalesced so one sync
0bfa3f46f0beatagen2026-09-26 09:12 UTC331
touching fifty rooms is one event. a left room stays in the list with its
0bfa3f46f0beatagen2026-09-26 09:12 UTC332
membership until it's forgotten. push rule changes never touch room info, so
0bfa3f46f0beatagen2026-09-26 09:12 UTC333
`NotificationSettingsChanged` brings a `RoomsChanged` along for the modes in
0bfa3f46f0beatagen2026-09-26 09:12 UTC334
the summaries
31ce2453c82fatagen2026-09-24 13:07 UTC335
31ce2453c82fatagen2026-09-24 13:07 UTC336
## verification
31ce2453c82fatagen2026-09-24 13:07 UTC337
5a52012c9b1datagen2026-09-24 23:45 UTC338
`Accept`, `Confirm`, `Reject` and `Cancel`. a request another device sends
5a52012c9b1datagen2026-09-24 23:45 UTC339
shows up as `Incoming` and waits for `Accept`, or `Cancel` to turn it down. one
5a52012c9b1datagen2026-09-24 23:45 UTC340
is only taken up while nothing else is in flight. reject tells the other side
5a52012c9b1datagen2026-09-24 23:45 UTC341
the emoji didn't match. the watchers subscribe before they look at the current
5a52012c9b1datagen2026-09-24 23:45 UTC342
state, since the sdk only reports what changes after you ask. the watcher and
5a52012c9b1datagen2026-09-24 23:45 UTC343
the action paths claim the terminal event, so a racing
31ce2453c82fatagen2026-09-24 13:07 UTC344
cancel can't turn `Done` into `Cancelled`
31ce2453c82fatagen2026-09-24 13:07 UTC345
a319bae745d8atagen2026-09-26 09:59 UTC346
## mirror
a319bae745d8atagen2026-09-26 09:59 UTC347
a319bae745d8atagen2026-09-26 09:59 UTC348
the mirror is a consumer like any other, behind the `mirror` feature, and it
a319bae745d8atagen2026-09-26 09:59 UTC349
takes any stream of events rather than the `EventStream` itself so a host keeps
a319bae745d8atagen2026-09-26 09:59 UTC350
the raw events without a second channel. state, its sequence number and the
a319bae745d8atagen2026-09-26 09:59 UTC351
broadcast send all move under one lock, which is how `subscribe` gets a
a319bae745d8atagen2026-09-26 09:59 UTC352
snapshot and a receiver with nothing between them. a lagged subscriber starts
a319bae745d8atagen2026-09-26 09:59 UTC353
over from a fresh state under the same lock
a319bae745d8atagen2026-09-26 09:59 UTC354
a319bae745d8atagen2026-09-26 09:59 UTC355
nothing on the draining path waits for an engine answer. a resync runs on its
a319bae745d8atagen2026-09-26 09:59 UTC356
own task, retrying with backoff until a reset or a close for its timeline
a319bae745d8atagen2026-09-26 09:59 UTC357
aborts it. rooms are fetched on a task too, one at a time with one more owed at
0b8ce8dc9a1catagen2026-09-26 12:30 UTC358
most. a diff for a timeline with no reset is a divergence too, so a wrapper
0b8ce8dc9a1catagen2026-09-26 12:30 UTC359
that swallowed the reset still heals. anything a late subscriber needs, failures
0b8ce8dc9a1catagen2026-09-26 12:30 UTC360
included, lives in the state rather than only in an update
a319bae745d8atagen2026-09-26 09:59 UTC361
31ce2453c82fatagen2026-09-24 13:07 UTC362
## public model
31ce2453c82fatagen2026-09-24 13:07 UTC363
31ce2453c82fatagen2026-09-24 13:07 UTC364
owned ruma ids and typed data wherever there's a stable shape, `Raw` where
e9b6478f917fatagen2026-10-08 11:14 UTC365
arbitrary content has to survive. ruma is re-exported as `macltok::ruma`, so a
e9b6478f917fatagen2026-10-08 11:14 UTC366
host parses ids at the version we're typed against. anything expected to grow is
31ce2453c82fatagen2026-09-24 13:07 UTC367
`#[non_exhaustive]`. `Engine::from_client` stays public for people who built
31ce2453c82fatagen2026-09-24 13:07 UTC368
their own client