atagen

macltok

public
Star0
Watch0
9d584825009ddce6ae71cf151793e63b4902ed25
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
8fa8a7cde89catagen2026-10-08 12:39 UTC122
a custom emote reaction keys on the image's mxc uri and carries its shortcode
8fa8a7cde89catagen2026-10-08 12:39 UTC123
(msc4027), so clients without the pack can still label it. `Reaction` says
8fa8a7cde89catagen2026-10-08 12:39 UTC124
which image it is and the shortcode any sender gave, and `toggle_reaction`
8fa8a7cde89catagen2026-10-08 12:39 UTC125
takes the shortcode to send
8fa8a7cde89catagen2026-10-08 12:39 UTC126
66db1b3cd895atagen2026-10-25 10:28 UTC127
## thread lists
66db1b3cd895atagen2026-10-25 10:28 UTC128
66db1b3cd895atagen2026-10-25 10:28 UTC129
a room's thread list is its own thing, not a timeline and not the room actor's.
66db1b3cd895atagen2026-10-25 10:28 UTC130
it needs only the room, so a room needn't be open for its list. one worker holds
66db1b3cd895atagen2026-10-25 10:28 UTC131
every open list. `Open` sends an empty `ThreadListReset`, `Paginate` asks
66db1b3cd895atagen2026-10-25 10:28 UTC132
`/rooms/{id}/threads` through the sdk's `Room::list_threads`, and the list
66db1b3cd895atagen2026-10-25 10:28 UTC133
follows the room's event cache for as long as it's open. changes go out as
66db1b3cd895atagen2026-10-25 10:28 UTC134
upserts keyed by root, which `ThreadListEntry::upsert` puts in order, newest
66db1b3cd895atagen2026-10-25 10:28 UTC135
activity first and ties by root, so a list can't diverge the way an index diff
66db1b3cd895atagen2026-10-25 10:28 UTC136
can
66db1b3cd895atagen2026-10-25 10:28 UTC137
66db1b3cd895atagen2026-10-25 10:28 UTC138
the sdk's `ThreadListService` was passed over. it never adds a thread that wasn't
66db1b3cd895atagen2026-10-25 10:28 UTC139
on a page already loaded, never reorders, and adds one to the count for every
66db1b3cd895atagen2026-10-25 10:28 UTC140
event the cache hands it, replays and back pagination included. here a reply
e7c1b037154datagen2026-10-25 13:50 UTC141
counts once, by id, when sync just brought it, it's newer than the thread's
e7c1b037154datagen2026-10-25 13:50 UTC142
latest and it was sent after the count was read off the server, all by the sdk's
e7c1b037154datagen2026-10-25 13:50 UTC143
corrected timestamp. the latest alone can't date a count. a summary from the
e7c1b037154datagen2026-10-25 13:50 UTC144
event cache names its latest reply without the event, and continuwuity 26.9
e7c1b037154datagen2026-10-25 13:50 UTC145
bundles the latest as bare content, so with only that check every reply the
e7c1b037154datagen2026-10-25 13:50 UTC146
cache re-inserted read as new and a page's count doubled. a newer reply that
97fb5720d881atagen2026-10-25 14:15 UTC147
doesn't count still becomes the latest, and an entry built without a dated
97fb5720d881atagen2026-10-25 14:15 UTC148
latest takes the newest reply the event cache holds. replays, reloads from the store and
e7c1b037154datagen2026-10-25 13:50 UTC149
back pagination never count. a reply sync brings to a thread the list hasn't
e7c1b037154datagen2026-10-25 13:50 UTC150
got fetches its root, four at a time, and adds it. one from anywhere else
e7c1b037154datagen2026-10-25 13:50 UTC151
doesn't, since that
21b9227729a4atagen2026-10-25 11:03 UTC152
says nothing about the thread being active, and a root the server doesn't have
21b9227729a4atagen2026-10-25 11:03 UTC153
isn't asked for again. a redacted reply doesn't uncount, an edit doesn't change
21b9227729a4atagen2026-10-25 11:03 UTC154
the latest reply's preview, and a preview's own reply target comes back
21b9227729a4atagen2026-10-25 11:03 UTC155
`Unavailable` rather than fetched
21b9227729a4atagen2026-10-25 11:03 UTC156
21b9227729a4atagen2026-10-25 11:03 UTC157
a lagged listener can't know what it missed, so the list fetches its first page
21b9227729a4atagen2026-10-25 11:03 UTC158
again and sends a `ThreadListReset` with it. leaving the room, or being kicked
60454002af5fatagen2026-10-25 11:13 UTC159
or banned, closes the list with `ThreadListClosed` and `RoomLeft`, the shape an
60454002af5fatagen2026-10-25 11:13 UTC160
event context closing on a leave has. `Close` sends one too, with `Requested`,
60454002af5fatagen2026-10-25 11:13 UTC161
since a mirror holds the list and has to drop it. a list's media belongs to its
60454002af5fatagen2026-10-25 11:13 UTC162
own scope and its ids carry it, like an event context's, bounded the same way so
60454002af5fatagen2026-10-25 11:13 UTC163
a long open list sheds the previews its threads moved past, and released on
60454002af5fatagen2026-10-25 11:13 UTC164
close
66db1b3cd895atagen2026-10-25 10:28 UTC165
38c4caecdb3datagen2026-09-26 13:35 UTC166
## warming up to send
38c4caecdb3datagen2026-09-26 13:35 UTC167
38c4caecdb3datagen2026-09-26 13:35 UTC168
the sdk does all its key work on the send path, in the room's send queue before
38c4caecdb3datagen2026-09-26 13:35 UTC169
the `/send` itself. members synced, a `/keys/query` for anyone untracked or
38c4caecdb3datagen2026-09-26 13:35 UTC170
dirty, then a `/keys/claim` and the room key sent to every device. over a slow
dc609022bb53atagen2026-10-08 12:39 UTC171
federation that's a minute of nothing. `prepare_to_send` runs all of it ahead
dc609022bb53atagen2026-10-08 12:39 UTC172
of time through the fork's `Room::prepare_encryption`, the same function the
dc609022bb53atagen2026-10-08 12:39 UTC173
send queue calls. the share sits behind the sdk's per-room dedup, so a send
dc609022bb53atagen2026-10-08 12:39 UTC174
landing mid-warm-up waits for it rather than sharing again, and a send after it
dc609022bb53atagen2026-10-08 12:39 UTC175
finds nothing left to do but the `/send`. a room whose encryption isn't known
dc609022bb53atagen2026-10-08 12:39 UTC176
yet gets asked, which the send would have done anyway. one warm-up per room at
dc609022bb53atagen2026-10-08 12:39 UTC177
a time, a second ask joins it. opening a room or starting to type runs one too,
dc609022bb53atagen2026-10-08 12:39 UTC178
unless `EngineConfig::prepare_to_send` is off. it's on because malice asked,
dc609022bb53atagen2026-10-08 12:39 UTC179
not because anyone decided
dc609022bb53atagen2026-10-08 12:39 UTC180
dc609022bb53atagen2026-10-08 12:39 UTC181
the session rotates after 100 messages or a week, and sharing isn't a message,
dc609022bb53atagen2026-10-08 12:39 UTC182
so a warm-up is only wasted on a room left idle about that long
38c4caecdb3datagen2026-09-26 13:35 UTC183
31ce2453c82fatagen2026-09-24 13:07 UTC184
## mutations
31ce2453c82fatagen2026-09-24 13:07 UTC185
31ce2453c82fatagen2026-09-24 13:07 UTC186
membership, moderation, power levels, room creation and account data writes go
31ce2453c82fatagen2026-09-24 13:07 UTC187
through one keyed fifo scheduler. same room, same alias, same dm partner or
31ce2453c82fatagen2026-09-24 13:07 UTC188
same account data type run in order, everything else runs at once.
31ce2453c82fatagen2026-09-24 13:07 UTC189
`create_direct` hands back the existing dm when there is one
31ce2453c82fatagen2026-09-24 13:07 UTC190
31ce2453c82fatagen2026-09-24 13:07 UTC191
notifications and encryption each get one serial worker. the encryption worker
31ce2453c82fatagen2026-09-24 13:07 UTC192
owns the verification controller outright
31ce2453c82fatagen2026-09-24 13:07 UTC193
31ce2453c82fatagen2026-09-24 13:07 UTC194
power level changes build one state event from the local mapping, since the
31ce2453c82fatagen2026-09-24 13:07 UTC195
sdk's own patcher is private
31ce2453c82fatagen2026-09-24 13:07 UTC196
31ce2453c82fatagen2026-09-24 13:07 UTC197
## media
31ce2453c82fatagen2026-09-24 13:07 UTC198
5d2974de5826atagen2026-10-08 11:29 UTC199
media is a read. `ClientHandle::media` takes a `MediaId`, from a retained
5d2974de5826atagen2026-10-08 11:29 UTC200
timeline or an avatar, through one bounded
5a52012c9b1datagen2026-09-24 23:45 UTC201
download path with the byte cap the sdk's own download lacks, an hour's total
5a52012c9b1datagen2026-09-24 23:45 UTC202
deadline instead of the sdk client's thirty seconds, and a minute's patience
5a52012c9b1datagen2026-09-24 23:45 UTC203
for a stalled stream. concurrent asks for the same bytes share one flight.
5a52012c9b1datagen2026-09-24 23:45 UTC204
a thumbnail the server won't make falls back to the file itself.
5a52012c9b1datagen2026-09-24 23:45 UTC205
`MediaId` parses from a string, since a web client has to round trip it
31ce2453c82fatagen2026-09-24 13:07 UTC206
31ce2453c82fatagen2026-09-24 13:07 UTC207
the cache holds plaintext, keyed like the sdk keys it, so the sdk and macltok
5a52012c9b1datagen2026-09-24 23:45 UTC208
can read each other's entries. that's how your own encrypted uploads come back.
5a52012c9b1datagen2026-09-24 23:45 UTC209
the sdk keys plain and encrypted sources alike, so a plain event pointing at
5a52012c9b1datagen2026-09-24 23:45 UTC210
someone's encrypted file can leave ciphertext in the cache. an encrypted read
5a52012c9b1datagen2026-09-24 23:45 UTC211
checks the cached bytes against the file's hash and decrypts them if they match
31ce2453c82fatagen2026-09-24 13:07 UTC212
31ce2453c82fatagen2026-09-24 13:07 UTC213
media ids are registered as timelines render and forgotten when their timeline
31ce2453c82fatagen2026-09-24 13:07 UTC214
closes, least recently seen first once the cap is hit
31ce2453c82fatagen2026-09-24 13:07 UTC215
5d2974de5826atagen2026-10-08 11:29 UTC216
avatars are `MediaId`s too, so a host never handles an mxc uri. they aren't
5d2974de5826atagen2026-10-08 11:29 UTC217
registered, since they live in the room list and member lists for the whole
5d2974de5826atagen2026-10-08 11:29 UTC218
session and there can be thousands, and the cap would evict ones still on
5d2974de5826atagen2026-10-08 11:29 UTC219
screen. a plain avatar carries no keys, so its id is its mxc uri and resolves
5d2974de5826atagen2026-10-08 11:29 UTC220
without the registry. it never expires, and an mxc uri a host got some other way
5d2974de5826atagen2026-10-08 11:29 UTC221
parses into one. `set_avatar` still takes an mxc uri
5d2974de5826atagen2026-10-08 11:29 UTC222
5b21b919d1e6atagen2026-09-27 11:47 UTC223
streaming decryption inside macltok was declined. in a browser the service
5b21b919d1e6atagen2026-09-27 11:47 UTC224
worker answering range requests is a separate context from the worker running
5b21b919d1e6atagen2026-09-27 11:47 UTC225
macltok, and streaming through macltok would carry every chunk through wasm
5b21b919d1e6atagen2026-09-27 11:47 UTC226
memory, the very thing the host streams to avoid. `media_source` hands over the
5b21b919d1e6atagen2026-09-27 11:47 UTC227
url and keys `media` would have used, picked the same way, and `access_token`
5b21b919d1e6atagen2026-09-27 11:47 UTC228
the rest. the byte cap doesn't apply, nothing is buffered
5b21b919d1e6atagen2026-09-27 11:47 UTC229
411bc1c78b65atagen2026-10-22 07:39 UTC230
uploads are commands that answer once the send queue has them. the fork puts
411bc1c78b65atagen2026-10-22 07:39 UTC231
upload progress on the client-wide send queue channel too, which upstream keeps
411bc1c78b65atagen2026-10-22 07:39 UTC232
for each room's own, so one watcher there follows every attachment in the queue,
411bc1c78b65atagen2026-10-22 07:39 UTC233
leftovers the sdk resumes from last time included, and ends each with one
411bc1c78b65atagen2026-10-22 07:39 UTC234
`MediaUploadFinished`. sent, cancelled, its room forgotten, or
0bfa3f46f0beatagen2026-09-26 09:12 UTC235
missed when that channel overflowed at the wrong moment, since then there's no
0bfa3f46f0beatagen2026-09-26 09:12 UTC236
knowing which. a failure isn't an ending, a wedged upload can still be retried.
0bfa3f46f0beatagen2026-09-26 09:12 UTC237
uploads don't belong to a room's actor, so closing the room leaves them be
31ce2453c82fatagen2026-09-24 13:07 UTC238
411bc1c78b65atagen2026-10-22 07:39 UTC239
that watcher is the only thing speaking about uploads. anything that ever
411bc1c78b65atagen2026-10-22 07:39 UTC240
showed progress or a failure is followed until it gets exactly one
411bc1c78b65atagen2026-10-22 07:39 UTC241
`MediaUploadFinished`, and nothing about it goes out after. the sdk sends
411bc1c78b65atagen2026-10-22 07:39 UTC242
progress from a detached task, so a tick can turn up behind its own upload's
411bc1c78b65atagen2026-10-22 07:39 UTC243
end, and the watcher remembers the last few hundred ends to drop those. the
7cf348521204atagen2026-09-26 12:28 UTC244
engine stopping ends nothing, `Stopped` covers it
7c26fad34822atagen2026-09-26 09:41 UTC245
31ce2453c82fatagen2026-09-24 13:07 UTC246
## sessions and secrets
31ce2453c82fatagen2026-09-24 13:07 UTC247
31ce2453c82fatagen2026-09-24 13:07 UTC248
`Session::login_password`, `begin_oauth` and `restore` share one client builder
62fe32759529atagen2026-09-26 08:50 UTC249
with an encrypted store, refresh tokens and threads. the store is sqlite
62fe32759529atagen2026-09-26 08:50 UTC250
natively and indexeddb in a browser. secret-bearing inputs don't derive `Clone`
31ce2453c82fatagen2026-09-24 13:07 UTC251
9c05b1dd327aatagen2026-10-08 11:27 UTC252
the homeserver a person typed is checked before the client sees it, and a bad
9c05b1dd327aatagen2026-10-08 11:27 UTC253
one is `AccountError::InvalidHomeserver`. a bare server name passes untouched,
9c05b1dd327aatagen2026-10-08 11:27 UTC254
since that's what sends the sdk to `.well-known`, and a host that "helpfully"
9c05b1dd327aatagen2026-10-08 11:27 UTC255
adds `https://` turns `example.org` into a homeserver url and skips discovery.
9c05b1dd327aatagen2026-10-08 11:27 UTC256
a url needs a host, no userinfo, and https. plain http needs
9c05b1dd327aatagen2026-10-08 11:27 UTC257
`allow_insecure_loopback` and a loopback host. `SoftLogout::password_login` sets
9c05b1dd327aatagen2026-10-08 11:27 UTC258
it, since its homeserver passed once already
9c05b1dd327aatagen2026-10-08 11:27 UTC259
713205a95af2atagen2026-09-26 09:24 UTC260
a `SessionStore` owns one login's record and its store. login saves the first
f12377bce9b9atagen2026-09-26 12:32 UTC261
record, `restore` loads it, and the engine a `Session` builds saves every token
f12377bce9b9atagen2026-09-26 12:32 UTC262
rotation after that, since a refresh token works once and a host that missed one
f12377bce9b9atagen2026-09-26 12:32 UTC263
save has lost the login. the session changes are subscribed the moment the
f12377bce9b9atagen2026-09-26 12:32 UTC264
client exists, because the sdk can refresh during restore before any engine
f12377bce9b9atagen2026-09-26 12:32 UTC265
does. a save runs off the event stream, so a full stream never holds it back,
f12377bce9b9atagen2026-09-26 12:32 UTC266
and `SessionTokensChanged` only goes out once it has landed, or
f12377bce9b9atagen2026-09-26 12:32 UTC267
`SessionSaveFailed` if it didn't. natively the sdk's save callback runs inside
f12377bce9b9atagen2026-09-26 12:32 UTC268
the refresh, before the new token is used, and `save_blocking` writes there and
f12377bce9b9atagen2026-09-26 12:32 UTC269
then. in a browser there's only the async save, so a tab that dies between a
0291516d40b5atagen2026-10-22 08:31 UTC270
refresh and its save wakes up holding a spent token. a fork patch letting the
0291516d40b5atagen2026-10-22 08:31 UTC271
sdk await an async save before it uses the new token would close that, but it
0291516d40b5atagen2026-10-22 08:31 UTC272
changes how a refresh behaves rather than what's visible, and on 2026-10-22 we
0291516d40b5atagen2026-10-22 08:31 UTC273
held off so the fork doesn't drift that far that fast. upstream taking an async
0291516d40b5atagen2026-10-22 08:31 UTC274
save callback, or the fork already carrying behaviour changes, reopens it.
0291516d40b5atagen2026-10-22 08:31 UTC275
`without_persistence`
f12377bce9b9atagen2026-09-26 12:32 UTC276
hands the job back to the host. `EncryptedSessionStore` seals the record with
713205a95af2atagen2026-09-26 09:24 UTC277
the sdk's `StoreCipher` under the store's passphrase and replaces it by rename,
713205a95af2atagen2026-09-26 09:24 UTC278
so a crash mid save leaves the old one. it keeps the record out of the sdk's
713205a95af2atagen2026-09-26 09:24 UTC279
state store because sqlite writes custom values there in the clear, and a
713205a95af2atagen2026-09-26 09:24 UTC280
client can't open its store before it knows the homeserver the record holds.
713205a95af2atagen2026-09-26 09:24 UTC281
`discard` deletes the record, then the store, since a record without its store
6f1d6591c10datagen2026-09-26 09:52 UTC282
would restore into empty crypto. the host follows `Termination::retain`.
6f1d6591c10datagen2026-09-26 09:52 UTC283
`Nothing` discards. `Store` calls `soft_log_out`, which keeps the store and
6f1d6591c10datagen2026-09-26 09:52 UTC284
swaps the record for one holding the homeserver, user, device and oauth client
6f1d6591c10datagen2026-09-26 09:52 UTC285
id with no tokens. `restore` hands that back as `Restore::SoftLoggedOut`, so a
6f1d6591c10datagen2026-09-26 09:52 UTC286
restart still knows which device to log in again as, and the login overwrites
a250c7fa43fbatagen2026-09-26 13:35 UTC287
it. one store directory per login. a login whose first save fails comes back as
a250c7fa43fbatagen2026-09-26 13:35 UTC288
`AccountError::Unsaved` holding the live session, so the host retries the save
a250c7fa43fbatagen2026-09-26 13:35 UTC289
rather than spending another device
a250c7fa43fbatagen2026-09-26 13:35 UTC290
cb0668d30c14atagen2026-10-08 11:34 UTC291
`Accounts` picks the store, so a host never decides which account a login form
cb0668d30c14atagen2026-10-08 11:34 UTC292
means. it's one owner's accounts under one root and one passphrase, a store and
cb0668d30c14atagen2026-10-08 11:34 UTC293
record each, named by a random id. a password login that names a
cb0668d30c14atagen2026-10-08 11:34 UTC294
soft-logged-out account, by localpart or full user id on the same server, logs
cb0668d30c14atagen2026-10-08 11:34 UTC295
back in as that device into that store. the server is compared by name as well
cb0668d30c14atagen2026-10-08 11:34 UTC296
as by url, since a typed `example.org` is stored as wherever discovery led. a
cb0668d30c14atagen2026-10-08 11:34 UTC297
live account it names is `AlreadyLoggedIn`, anyone else gets a new store. oauth
cb0668d30c14atagen2026-10-08 11:34 UTC298
can't know who it's for until it finishes, so it resumes only the device
cb0668d30c14atagen2026-10-08 11:34 UTC299
`SoftLogout::oauth_login` names. a new store is locked before it's listed, so
cb0668d30c14atagen2026-10-08 11:34 UTC300
`list` can sweep stores with no record, left by a login that died or a discard
cb0668d30c14atagen2026-10-08 11:34 UTC301
that didn't finish, without catching one mid login. a browser lists accounts in
cb0668d30c14atagen2026-10-08 11:34 UTC302
an IndexedDB database of their own, a key each, so two tabs adding at once
cb0668d30c14atagen2026-10-08 11:34 UTC303
don't lose one
cb0668d30c14atagen2026-10-08 11:34 UTC304
cb0668d30c14atagen2026-10-08 11:34 UTC305
the set is per owner because resuming needs only the password, and the store
cb0668d30c14atagen2026-10-08 11:34 UTC306
holds the device's e2ee keys. a set shared across people would hand someone who
cb0668d30c14atagen2026-10-08 11:34 UTC307
knows a password the history a fresh login wouldn't get
cb0668d30c14atagen2026-10-08 11:34 UTC308
a250c7fa43fbatagen2026-09-26 13:35 UTC309
one engine per store. before the client opens the store we take the platform's
a250c7fa43fbatagen2026-09-26 13:35 UTC310
own exclusive lock on it, `File::try_lock` on `macltok.lock` in the store
a250c7fa43fbatagen2026-09-26 13:35 UTC311
directory natively and a Web Lock named after the store in a browser. the
a250c7fa43fbatagen2026-09-26 13:35 UTC312
engine and every `ClientHandle` share it, so it goes when the last of them
a250c7fa43fbatagen2026-09-26 13:35 UTC313
does, since a lingering handle still has the client and its store open. anyone
a250c7fa43fbatagen2026-09-26 13:35 UTC314
else, in this process, another one or another tab, gets `StoreInUse` at once,
a250c7fa43fbatagen2026-09-26 13:35 UTC315
so the host decides whether to wait, hand over or say so. the os or the browser
a250c7fa43fbatagen2026-09-26 13:35 UTC316
lets go when the holder dies. a SharedWorker serving every tab from one engine
a250c7fa43fbatagen2026-09-26 13:35 UTC317
never contends for it. Web Locks only exist in a secure context, so a browser
a250c7fa43fbatagen2026-09-26 13:35 UTC318
store needs one too
a250c7fa43fbatagen2026-09-26 13:35 UTC319
a250c7fa43fbatagen2026-09-26 13:35 UTC320
the sdk's cross process lease can't do this job. in matrix-sdk-common 0.18
a250c7fa43fbatagen2026-09-26 13:35 UTC321
`CrossProcessLock::try_lock_once` (`cross_process_lock.rs` 389-448) leases for
a250c7fa43fbatagen2026-09-26 13:35 UTC322
500ms and renews every 50ms from a spawned task, which gives up for good on one
a250c7fa43fbatagen2026-09-26 13:35 UTC323
failed renewal or on finding the lease lapsed and retaken, and tells no one. a
a250c7fa43fbatagen2026-09-26 13:35 UTC324
stall past half a second, ordinary in a tab and certain during the record's
a250c7fa43fbatagen2026-09-26 13:35 UTC325
PBKDF2, or one `SQLITE_BUSY`, hands the store to a second engine while the
a250c7fa43fbatagen2026-09-26 13:35 UTC326
first still thinks it's alone. with our lock keeping everyone else out, clients
a250c7fa43fbatagen2026-09-26 13:35 UTC327
run with `CrossProcessLockConfig::SingleProcess` and the sdk's own sections skip
a250c7fa43fbatagen2026-09-26 13:35 UTC328
the lease writes and renewal tasks
a250c7fa43fbatagen2026-09-26 13:35 UTC329
d8d8a9fe770aatagen2026-10-22 07:44 UTC330
a pending OAuth login survives the page going away for its redirect.
d8d8a9fe770aatagen2026-10-22 07:44 UTC331
`begin_oauth` saves it as the store's record, sealed like any other since it
d8d8a9fe770aatagen2026-10-22 07:44 UTC332
holds the PKCE verifier, through the fork's `OAuth::pending_authorization`.
d8d8a9fe770aatagen2026-10-22 07:44 UTC333
`Accounts::finish_oauth` takes the callback url and finds the account by its
d8d8a9fe770aatagen2026-10-22 07:44 UTC334
`state`, so the host keeps nothing across the redirect, and
d8d8a9fe770aatagen2026-10-22 07:44 UTC335
`Session::finish_oauth` does the same for one store. a host that keeps the
d8d8a9fe770aatagen2026-10-22 07:44 UTC336
process alive can still use `PendingOAuthLogin::finish`. while it waits,
d8d8a9fe770aatagen2026-10-22 07:44 UTC337
`restore` answers `Restore::OAuthPending`. the record lives in the store
d8d8a9fe770aatagen2026-10-22 07:44 UTC338
rather than with the host because `list` sweeps a store with no record, which
d8d8a9fe770aatagen2026-10-22 07:44 UTC339
would delete a fresh login's store mid redirect
d8d8a9fe770aatagen2026-10-22 07:44 UTC340
d8d8a9fe770aatagen2026-10-22 07:44 UTC341
the pending record keeps whatever the store held before, so a login resuming a
d8d8a9fe770aatagen2026-10-22 07:44 UTC342
soft-logged-out device that never comes back puts that device back, and a
d8d8a9fe770aatagen2026-10-22 07:44 UTC343
password login can still resume it meanwhile. abort, a failed finish after the
d8d8a9fe770aatagen2026-10-22 07:44 UTC344
redirect and an hour's wait all undo the login, a new account's store deleted
d8d8a9fe770aatagen2026-10-22 07:44 UTC345
and an old record put back. a failed `PendingOAuthLogin::finish` still holds
d8d8a9fe770aatagen2026-10-22 07:44 UTC346
the client, so it puts back an old record and leaves a new one to the hour.
d8d8a9fe770aatagen2026-10-22 07:44 UTC347
codes live minutes, so an hour is generous
31ce2453c82fatagen2026-09-24 13:07 UTC348
960ac0721079atagen2026-09-25 00:24 UTC349
## room lists, spaces and profiles
960ac0721079atagen2026-09-25 00:24 UTC350
960ac0721079atagen2026-09-25 00:24 UTC351
`RoomSummary` has what a room list row needs without a second query. topic,
960ac0721079atagen2026-09-25 00:24 UTC352
alias, member counts, heroes for nameless rooms, the inviter, where an upgraded
960ac0721079atagen2026-09-25 00:24 UTC353
room went, a latest event preview, the joined spaces listing it, and for a
960ac0721079atagen2026-09-25 00:24 UTC354
space the children it lists
960ac0721079atagen2026-09-25 00:24 UTC355
960ac0721079atagen2026-09-25 00:24 UTC356
parents come from one lazily built `SpaceService` shared by every handle.
960ac0721079atagen2026-09-25 00:24 UTC357
children come straight from the space's local state, joined or not.
960ac0721079atagen2026-09-25 00:24 UTC358
`space_hierarchy` pages through the server's view for browsing rooms you
960ac0721079atagen2026-09-25 00:24 UTC359
haven't joined, and holds its place between pages
960ac0721079atagen2026-09-25 00:24 UTC360
960ac0721079atagen2026-09-25 00:24 UTC361
`own_profile` and `user_profile` read profiles. name and avatar changes are
960ac0721079atagen2026-09-25 00:24 UTC362
commands, and uploading an avatar gets the same byte cap as any upload
960ac0721079atagen2026-09-25 00:24 UTC363
31ce2453c82fatagen2026-09-24 13:07 UTC364
## notifications and room data
31ce2453c82fatagen2026-09-24 13:07 UTC365
31ce2453c82fatagen2026-09-24 13:07 UTC366
the notification client is built lazily and shares the engine's sync service.
31ce2453c82fatagen2026-09-24 13:07 UTC367
sdk ui types stay behind the boundary
31ce2453c82fatagen2026-09-24 13:07 UTC368
31ce2453c82fatagen2026-09-24 13:07 UTC369
`rooms` normalises on demand with bounded concurrency and sorts by latest event,
31ce2453c82fatagen2026-09-24 13:07 UTC370
then name, then id. `RoomsChanged` is an invalidation, coalesced so one sync
21b9227729a4atagen2026-10-25 11:03 UTC371
touching fifty rooms is one event. a sync's room info lands before the event
21b9227729a4atagen2026-10-25 11:03 UTC372
cache has stored its events, so the watcher waits a tenth of a second after
21b9227729a4atagen2026-10-25 11:03 UTC373
anything wakes it and takes the lot as one. an event cache slower than that
21b9227729a4atagen2026-10-25 11:03 UTC374
costs a second one. a left room stays in the list with its
0bfa3f46f0beatagen2026-09-26 09:12 UTC375
membership until it's forgotten. push rule changes never touch room info, so
0bfa3f46f0beatagen2026-09-26 09:12 UTC376
`NotificationSettingsChanged` brings a `RoomsChanged` along for the modes in
0bfa3f46f0beatagen2026-09-26 09:12 UTC377
the summaries
31ce2453c82fatagen2026-09-24 13:07 UTC378
e5c6427adb35atagen2026-10-25 10:23 UTC379
with threads on, the sdk leaves thread events out of a room's counts and keeps
21b9227729a4atagen2026-10-25 11:03 UTC380
none of its own for them. `thread_unreads` counts them per root from the event
21b9227729a4atagen2026-10-25 11:03 UTC381
cache store, which keeps every reply it ever took whatever it holds in memory,
21b9227729a4atagen2026-10-25 11:03 UTC382
so closing a room, the cache trimming itself or a restart leaves them be. a
21b9227729a4atagen2026-10-25 11:03 UTC383
reply counts once it's newer than a receipt for its thread (msc3771), an
21b9227729a4atagen2026-10-25 11:03 UTC384
unthreaded one, or the user's own reply. a main-timeline receipt doesn't reach
21b9227729a4atagen2026-10-25 11:03 UTC385
into threads. the store has lost stream order between a thread and the rest of
21b9227729a4atagen2026-10-25 11:03 UTC386
the room, so an event's timestamp stands for its place, and a receipt on an
21b9227729a4atagen2026-10-25 11:03 UTC387
event the store never had falls back to the receipt's own time. a thread's
21b9227729a4atagen2026-10-25 11:03 UTC388
mentions also count in the room's `unread_mentions`, so nobody misses one,
21b9227729a4atagen2026-10-25 11:03 UTC389
while its messages and notifications stay apart, since they don't move the main
21b9227729a4atagen2026-10-25 11:03 UTC390
timeline's read marker
21b9227729a4atagen2026-10-25 11:03 UTC391
21b9227729a4atagen2026-10-25 11:03 UTC392
the counts are kept in the state store per room, only for threads with
21b9227729a4atagen2026-10-25 11:03 UTC393
something unread, so `rooms` reads them without touching the event cache and a
21b9227729a4atagen2026-10-25 11:03 UTC394
cold room never gets one made for it. thread events and threaded receipts leave
21b9227729a4atagen2026-10-25 11:03 UTC395
room info alone, so the room list watcher recounts on its own. a synced thread
21b9227729a4atagen2026-10-25 11:03 UTC396
event names the thread to count, an event cache update recounts the room's
21b9227729a4atagen2026-10-25 11:03 UTC397
threads already holding something unread, since a late decryption can turn up
21b9227729a4atagen2026-10-25 11:03 UTC398
a mention, and so does a receipt of the user's own. counting in the sdk would
21b9227729a4atagen2026-10-25 11:03 UTC399
be cheaper, but that's a behaviour change in the fork
e5c6427adb35atagen2026-10-25 10:23 UTC400
0291516d40b5atagen2026-10-22 08:31 UTC401
## link previews
0291516d40b5atagen2026-10-22 08:31 UTC402
0291516d40b5atagen2026-10-22 08:31 UTC403
a text message can carry its sender's own previews (msc4095), and those come
0291516d40b5atagen2026-10-22 08:31 UTC404
out on `Message::link_previews` without asking anyone, so they work in
0291516d40b5atagen2026-10-22 08:31 UTC405
encrypted rooms and leak nothing. an encrypted preview image is registered like
0291516d40b5atagen2026-10-22 08:31 UTC406
an attachment. a preview whose url isn't in the body is dropped, or a sender
0291516d40b5atagen2026-10-22 08:31 UTC407
could caption any link with any card
0291516d40b5atagen2026-10-22 08:31 UTC408
0291516d40b5atagen2026-10-22 08:31 UTC409
`url_preview` asks the homeserver, which fetches the page, re-hosts `og:image`
0291516d40b5atagen2026-10-22 08:31 UTC410
and answers with loose `og:` keys. that tells the homeserver every url, so
0291516d40b5atagen2026-10-22 08:31 UTC411
whether encrypted rooms ask is the host's call. answers are kept an hour and
0291516d40b5atagen2026-10-22 08:31 UTC412
misses ten minutes, and asks for one url at once share a request, since synapse
0291516d40b5atagen2026-10-22 08:31 UTC413
rate limits the endpoint and a timeline repeats links. synapse answers a page
0291516d40b5atagen2026-10-22 08:31 UTC414
it couldn't download with a 500, a dns failure with a 502 and its blocklist with
0291516d40b5atagen2026-10-22 08:31 UTC415
a 403, so those, 400 and 404 mean no preview. a genuine 500 hides in there too,
0291516d40b5atagen2026-10-22 08:31 UTC416
but a missing card is cheap. a
0291516d40b5atagen2026-10-22 08:31 UTC417
server that doesn't preview at all says so with `M_UNRECOGNIZED` once and isn't
0291516d40b5atagen2026-10-22 08:31 UTC418
asked again. a 429 anywhere is `ErrorKind::RateLimited`. page text from either
0291516d40b5atagen2026-10-22 08:31 UTC419
source is trimmed, 256 characters for a title or site name and 1024 for a
0291516d40b5atagen2026-10-22 08:31 UTC420
description, and an image that isn't an mxc uri is dropped
0291516d40b5atagen2026-10-22 08:31 UTC421
0291516d40b5atagen2026-10-22 08:31 UTC422
`url_previews_allowed` follows element, so both agree. the user's own choice
0291516d40b5atagen2026-10-22 08:31 UTC423
for the room, then their account wide default, then room state an admin set.
0291516d40b5atagen2026-10-22 08:31 UTC424
the room state comes in with the rest through the fork's extra required state
0291516d40b5atagen2026-10-22 08:31 UTC425
17e042fbc331atagen2026-10-08 12:39 UTC426
## image packs
17e042fbc331atagen2026-10-08 12:39 UTC427
17e042fbc331atagen2026-10-08 12:39 UTC428
msc2545 packs come from the user's account data, from room state, and from the
17e042fbc331atagen2026-10-08 12:39 UTC429
rooms the user enabled everywhere. the room list asks the server for every
17e042fbc331atagen2026-10-08 12:39 UTC430
room's `im.ponies.room_emotes`, through the fork's extra required state, so
17e042fbc331atagen2026-10-08 12:39 UTC431
packs sit in the store like any other state and a change reaches us even across
17e042fbc331atagen2026-10-08 12:39 UTC432
a gap in the timeline. that's every joined room's packs on the first sync,
17e042fbc331atagen2026-10-08 12:39 UTC433
tens of kilobytes for a big one, and only changes after. asking only for open
17e042fbc331atagen2026-10-08 12:39 UTC434
rooms would lose the enabled packs of rooms nobody has open
17e042fbc331atagen2026-10-08 12:39 UTC435
17e042fbc331atagen2026-10-08 12:39 UTC436
`image_packs` reads all of it from the store and hands back the lot, global
17e042fbc331atagen2026-10-08 12:39 UTC437
packs and each room's own, and `ImagePacks::for_room` puts a picker's list in
17e042fbc331atagen2026-10-08 12:39 UTC438
order. an enabled room the user hasn't joined is skipped, there's no state for
17e042fbc331atagen2026-10-08 12:39 UTC439
it. one bad image costs that image and a pack with nothing usable is dropped,
17e042fbc331atagen2026-10-08 12:39 UTC440
since pack contents are whatever someone's client wrote. `ImagePacksChanged`
17e042fbc331atagen2026-10-08 12:39 UTC441
says a pack moved somewhere, from a state event, an account data change or a
17e042fbc331atagen2026-10-08 12:39 UTC442
membership change taking a room's packs with it. it's an invalidation, and the
17e042fbc331atagen2026-10-08 12:39 UTC443
mirror refetches on it like it does rooms, keeping the packs in its state
17e042fbc331atagen2026-10-08 12:39 UTC444
31ce2453c82fatagen2026-09-24 13:07 UTC445
## verification
31ce2453c82fatagen2026-09-24 13:07 UTC446
5a52012c9b1datagen2026-09-24 23:45 UTC447
`Accept`, `Confirm`, `Reject` and `Cancel`. a request another device sends
5a52012c9b1datagen2026-09-24 23:45 UTC448
shows up as `Incoming` and waits for `Accept`, or `Cancel` to turn it down. one
5a52012c9b1datagen2026-09-24 23:45 UTC449
is only taken up while nothing else is in flight. reject tells the other side
5a52012c9b1datagen2026-09-24 23:45 UTC450
the emoji didn't match. the watchers subscribe before they look at the current
5a52012c9b1datagen2026-09-24 23:45 UTC451
state, since the sdk only reports what changes after you ask. the watcher and
5a52012c9b1datagen2026-09-24 23:45 UTC452
the action paths claim the terminal event, so a racing
31ce2453c82fatagen2026-09-24 13:07 UTC453
cancel can't turn `Done` into `Cancelled`
31ce2453c82fatagen2026-09-24 13:07 UTC454
a319bae745d8atagen2026-09-26 09:59 UTC455
## mirror
a319bae745d8atagen2026-09-26 09:59 UTC456
a319bae745d8atagen2026-09-26 09:59 UTC457
the mirror is a consumer like any other, behind the `mirror` feature, and it
a319bae745d8atagen2026-09-26 09:59 UTC458
takes any stream of events rather than the `EventStream` itself so a host keeps
a319bae745d8atagen2026-09-26 09:59 UTC459
the raw events without a second channel. state, its sequence number and the
a319bae745d8atagen2026-09-26 09:59 UTC460
broadcast send all move under one lock, which is how `subscribe` gets a
a319bae745d8atagen2026-09-26 09:59 UTC461
snapshot and a receiver with nothing between them. a lagged subscriber starts
a319bae745d8atagen2026-09-26 09:59 UTC462
over from a fresh state under the same lock
a319bae745d8atagen2026-09-26 09:59 UTC463
a319bae745d8atagen2026-09-26 09:59 UTC464
nothing on the draining path waits for an engine answer. a resync runs on its
a319bae745d8atagen2026-09-26 09:59 UTC465
own task, retrying with backoff until a reset or a close for its timeline
a319bae745d8atagen2026-09-26 09:59 UTC466
aborts it. rooms are fetched on a task too, one at a time with one more owed at
0b8ce8dc9a1catagen2026-09-26 12:30 UTC467
most. a diff for a timeline with no reset is a divergence too, so a wrapper
0b8ce8dc9a1catagen2026-09-26 12:30 UTC468
that swallowed the reset still heals. anything a late subscriber needs, failures
0b8ce8dc9a1catagen2026-09-26 12:30 UTC469
included, lives in the state rather than only in an update
a319bae745d8atagen2026-09-26 09:59 UTC470
cff1b0a4eb30atagen2026-10-25 10:42 UTC471
## search
cff1b0a4eb30atagen2026-10-25 10:42 UTC472
cff1b0a4eb30atagen2026-10-25 10:42 UTC473
`search_messages` takes words and a scope of one room, a list of them or every
cff1b0a4eb30atagen2026-10-25 10:42 UTC474
joined room, and answers newest first. it opens a search the way a jump opens an
cff1b0a4eb30atagen2026-10-25 10:42 UTC475
event context, a session owning the media of every hit it hands out under its
cff1b0a4eb30atagen2026-10-25 10:42 UTC476
own bounded scope, paged with `search_more` and its cursor and freed with
cff1b0a4eb30atagen2026-10-25 10:42 UTC477
`close_search`. at most eight stay open and opening another closes the oldest,
e521a7177da3atagen2026-10-25 11:18 UTC478
so a host that never closes anything still holds a bounded amount, and the
e521a7177da3atagen2026-10-25 11:18 UTC479
closed one goes out as `SearchClosed`. a hit's media ids carry their search's
e521a7177da3atagen2026-10-25 11:18 UTC480
owner prefix like a context's, and stop resolving once it closes. encrypted
e521a7177da3atagen2026-10-25 11:18 UTC481
rooms can't use the server's `/search`, so everything goes through one local
e521a7177da3atagen2026-10-25 11:18 UTC482
index, unencrypted
cff1b0a4eb30atagen2026-10-25 10:42 UTC483
rooms too, so results and coverage mean the same thing everywhere. a hit comes
cff1b0a4eb30atagen2026-10-25 10:42 UTC484
back the way event context shows its target, latest edit applied, with byte
cff1b0a4eb30atagen2026-10-25 10:42 UTC485
ranges of the matched words in its text
cff1b0a4eb30atagen2026-10-25 10:42 UTC486
cff1b0a4eb30atagen2026-10-25 10:42 UTC487
the index is tantivy, but not the fork's matrix-sdk-search. that crate keeps its
cff1b0a4eb30atagen2026-10-25 10:42 UTC488
schema private, keeps an index per room, opens a fresh `IndexWriter` for every
cff1b0a4eb30atagen2026-10-25 10:42 UTC489
event, ranks by score, hands back bare event ids and turns mmap on, which wasm
cff1b0a4eb30atagen2026-10-25 10:42 UTC490
can't build. `IndexWriter` can't run in wasm either, since it spawns indexing
cff1b0a4eb30atagen2026-10-25 10:42 UTC491
threads and rayon pools. so macltok keeps its own segments. each flush writes an
cff1b0a4eb30atagen2026-10-25 10:42 UTC492
immutable segment with `SingleSegmentIndexWriter` into a `RamDirectory`, edits
cff1b0a4eb30atagen2026-10-25 10:42 UTC493
and redactions are tombstones in a bitset beside it, and segments of a size
cff1b0a4eb30atagen2026-10-25 10:42 UTC494
class merge through `merge_filtered_segments` once six pile up, dropping the
cff1b0a4eb30atagen2026-10-25 10:42 UTC495
tombstoned docs as they go. all of that is single threaded with the docstore's
e521a7177da3atagen2026-10-25 11:18 UTC496
dedicated compressor off. message text is indexed and not stored, a preview
e521a7177da3atagen2026-10-25 11:18 UTC497
reads the event back. the one exception is an edit waiting for its original,
e521a7177da3atagen2026-10-25 11:18 UTC498
whose text is kept so the original can take it over. a 2026-10-25 spike ran the
cff1b0a4eb30atagen2026-10-25 10:42 UTC499
whole cycle in a headless firefox worker
cff1b0a4eb30atagen2026-10-25 10:42 UTC500
e521a7177da3atagen2026-10-25 11:18 UTC501
it lives with the store, so it's per login and never synced, and it's only
e521a7177da3atagen2026-10-25 11:18 UTC502
opened for the user and homeserver that made it. natively it's
e521a7177da3atagen2026-10-25 11:18 UTC503
`macltok-search.sqlite3` beside the sdk's databases, created 0600, and in a
e521a7177da3atagen2026-10-25 11:18 UTC504
browser the `::macltok-search` IndexedDB database, and both go when the store is
e521a7177da3atagen2026-10-25 11:18 UTC505
deleted. every segment, every tombstone generation, the manifest and the waiting
e521a7177da3atagen2026-10-25 11:18 UTC506
undecryptable events are blobs sealed by a `StoreCipher` under the store
e521a7177da3atagen2026-10-25 11:18 UTC507
passphrase, stored under a keyed hash of their name like the mentions rows, the
e521a7177da3atagen2026-10-25 11:18 UTC508
name sealed inside so blobs can't be swapped. the manifest a commit saves is the
e521a7177da3atagen2026-10-25 11:18 UTC509
index as it stands once that commit lands, and blobs it doesn't point at are
e521a7177da3atagen2026-10-25 11:18 UTC510
dropped on the next load. one commit is one transaction, sqlite in wal with
e521a7177da3atagen2026-10-25 11:18 UTC511
normal sync and IndexedDB relaxed, so a crash can lose the last few seconds of
e521a7177da3atagen2026-10-25 11:18 UTC512
commits but never leaves half of one. the working copy lives in memory, all of
e521a7177da3atagen2026-10-25 11:18 UTC513
it, which is the price of tantivy's directory being synchronous and IndexedDB
e521a7177da3atagen2026-10-25 11:18 UTC514
not. a store that won't open or read, or five commits failing in a row, means
e521a7177da3atagen2026-10-25 11:18 UTC515
an index in memory for the rest of the engine, and coverage says
e521a7177da3atagen2026-10-25 11:18 UTC516
`persistent: false` with the reason in `in_memory`
cff1b0a4eb30atagen2026-10-25 10:42 UTC517
cff1b0a4eb30atagen2026-10-25 10:42 UTC518
it's fed from what the client sees. each joined room's event cache is followed
cff1b0a4eb30atagen2026-10-25 10:42 UTC519
from its first update, so sync, pagination and the redecryptor's replacements
cff1b0a4eb30atagen2026-10-25 10:42 UTC520
all arrive as diffs, and a room idle for five minutes is let go so the cache can
e521a7177da3atagen2026-10-25 11:18 UTC521
shrink. the generic update channel is followed from before the index loads, and
e521a7177da3atagen2026-10-25 11:18 UTC522
if it lags every joined room is followed, since a lost update could be a room's
e521a7177da3atagen2026-10-25 11:18 UTC523
only one. an edit lands on its original under the original's key, the latest by
e521a7177da3atagen2026-10-25 11:18 UTC524
the original's sender winning. one that arrives first waits under its target and
e521a7177da3atagen2026-10-25 11:18 UTC525
its own sender, out of results, until the original turns up and takes the
e521a7177da3atagen2026-10-25 11:18 UTC526
author's edit, so a stranger's early edit can't crowd it out. an edit of an edit
e521a7177da3atagen2026-10-25 11:18 UTC527
is ignored, and an edit to content we can't read clears the old text. a
e521a7177da3atagen2026-10-25 11:18 UTC528
redaction drops the key and is remembered for a while, so a stale copy can't
e521a7177da3atagen2026-10-25 11:18 UTC529
bring it back. redacting the latest edit reads the original again with whatever
e521a7177da3atagen2026-10-25 11:18 UTC530
edits of it are still around. a message still encrypted waits, raw, up to two
e521a7177da3atagen2026-10-25 11:18 UTC531
thousand of them in sixteen blobs, and is decrypted eight at a time when a room
e521a7177da3atagen2026-10-25 11:18 UTC532
key for its session arrives
cff1b0a4eb30atagen2026-10-25 10:42 UTC533
cff1b0a4eb30atagen2026-10-25 10:42 UTC534
backfill is `/messages` straight from the room, not through the event cache,
e521a7177da3atagen2026-10-25 11:18 UTC535
so it never grows a timeline someone has open, filtered to messages, encrypted
e521a7177da3atagen2026-10-25 11:18 UTC536
events, redactions, stickers and polls. one request at a time across every
e521a7177da3atagen2026-10-25 11:18 UTC537
room, two seconds apart, a hundred events a page and ten pages a room per
cff1b0a4eb30atagen2026-10-25 10:42 UTC538
engine run, round robin. a room walks back from the newest event to the start,
e521a7177da3atagen2026-10-25 11:18 UTC539
and resumes from its saved token next time. a room that's been indexed before
e521a7177da3atagen2026-10-25 11:18 UTC540
also gets a head pass the first time it shows up in a run, from the newest event
e521a7177da3atagen2026-10-25 11:18 UTC541
back until it meets something that was already indexed when the engine started,
e521a7177da3atagen2026-10-25 11:18 UTC542
which picks up what happened while we were offline. a failing room backs off on
e521a7177da3atagen2026-10-25 11:18 UTC543
its own from a minute to fifteen. a saved token the server turns down with a 400
e521a7177da3atagen2026-10-25 11:18 UTC544
is dropped and the room starts again from the newest. leaving a room cancels its
e521a7177da3atagen2026-10-25 11:18 UTC545
page. all of that is `EngineConfig`, and zero pages a room turns backfill off
cff1b0a4eb30atagen2026-10-25 10:42 UTC546
cff1b0a4eb30atagen2026-10-25 10:42 UTC547
coverage is per room, the oldest and newest indexed message, whether backfill
e521a7177da3atagen2026-10-25 11:18 UTC548
reached the start, and where backfill is at, plus the undecryptable messages in
e521a7177da3atagen2026-10-25 11:18 UTC549
scope. it comes with every page
e521a7177da3atagen2026-10-25 11:18 UTC550
e521a7177da3atagen2026-10-25 11:18 UTC551
the analyser makes a token of every run of letters and digits up to 64 chars,
e521a7177da3atagen2026-10-25 11:18 UTC552
so hashes and long words fit, and lowercases and folds accents, so `cafe` finds
e521a7177da3atagen2026-10-25 11:18 UTC553
`Café`. han, kana, hangul and thai have no spaces to split on, so every char is
e521a7177da3atagen2026-10-25 11:18 UTC554
a token and so is every pair of neighbours, and a query for a run needs all its
e521a7177da3atagen2026-10-25 11:18 UTC555
pairs. that finds a word inside a sentence at the cost of the odd match across a
e521a7177da3atagen2026-10-25 11:18 UTC556
word boundary. emoji are tokens too. queries have no field syntax, so `re:` or a
e521a7177da3atagen2026-10-25 11:18 UTC557
url is just its words. words all have to match, quotes make a phrase and `-`
e521a7177da3atagen2026-10-25 11:18 UTC558
excludes, and a query with nothing left to find is `Invalid` rather than
e521a7177da3atagen2026-10-25 11:18 UTC559
everything in scope. there's no stemming
cff1b0a4eb30atagen2026-10-25 10:42 UTC560
cff1b0a4eb30atagen2026-10-25 10:42 UTC561
a hit is read back when it's shown, `load_or_fetch_event` for the original and
cff1b0a4eb30atagen2026-10-25 10:42 UTC562
for its latest edit, which is the event cache and its store first and the
e521a7177da3atagen2026-10-25 11:18 UTC563
server's `/event` after, eight at a time. the original's bundled edit is ignored,
e521a7177da3atagen2026-10-25 11:18 UTC564
since the index already knows the newest one. one that's turned out redacted is
e521a7177da3atagen2026-10-25 11:18 UTC565
dropped from the page and from the index. one that can't be read at all, gone
e521a7177da3atagen2026-10-25 11:18 UTC566
from the cache with the server unreachable or refusing, is left out of that page
e521a7177da3atagen2026-10-25 11:18 UTC567
but stays indexed, so it shows up again once it can be read. the cursor still
e521a7177da3atagen2026-10-25 11:18 UTC568
moves past it
e521a7177da3atagen2026-10-25 11:18 UTC569
e521a7177da3atagen2026-10-25 11:18 UTC570
tantivy's merger times itself with `Instant::now()` whenever a `log` logger is on
e521a7177da3atagen2026-10-25 11:18 UTC571
at debug, and that panics in wasm. on 2026-10-25 we capped `log` at info at
e521a7177da3atagen2026-10-25 11:18 UTC572
compile time in wasm, `max_level_info` and `release_max_level_info` both, since
e521a7177da3atagen2026-10-25 11:18 UTC573
in release the release cap wins. it's all or nothing for the whole wasm build, so
e521a7177da3atagen2026-10-25 11:18 UTC574
a host gives up `log` debug and trace from every crate, not just tantivy, and
e521a7177da3atagen2026-10-25 11:18 UTC575
asking for any other cap is log's own "multiple max_level_* features set" error.
e521a7177da3atagen2026-10-25 11:18 UTC576
tantivy timing with something wasm can call, or reading `Instant` only behind
e521a7177da3atagen2026-10-25 11:18 UTC577
its own feature, reopens it
cff1b0a4eb30atagen2026-10-25 10:42 UTC578
dc609022bb53atagen2026-10-08 12:39 UTC579
## the sdk fork
dc609022bb53atagen2026-10-08 12:39 UTC580
dc609022bb53atagen2026-10-08 12:39 UTC581
we build against a fork of matrix-rust-sdk, branch `macltok`, cut from the
dc609022bb53atagen2026-10-08 12:39 UTC582
matrix-sdk 0.18.0 tag. turned down on 2026-09-26, taken up on 2026-10-22 once
7515819c2f3eatagen2026-10-22 13:03 UTC583
it was holding back features every peer client has. the sdk crates are git
7515819c2f3eatagen2026-10-22 13:03 UTC584
dependencies pinned to a fork rev, so dependents get the fork through macltok
7515819c2f3eatagen2026-10-22 13:03 UTC585
with no patch block of their own, and macltok stays off crates.io until
7515819c2f3eatagen2026-10-22 13:03 UTC586
upstream takes the patches. a dependent naming matrix-sdk itself has to point
7515819c2f3eatagen2026-10-22 13:03 UTC587
at the same rev or it gets a second sdk
dc609022bb53atagen2026-10-08 12:39 UTC588
dc609022bb53atagen2026-10-08 12:39 UTC589
it carries visibility and config changes, each small enough to be an upstream
dc609022bb53atagen2026-10-08 12:39 UTC590
pr, and nothing that changes how the sdk behaves. if that stops being true,
dc609022bb53atagen2026-10-08 12:39 UTC591
especially in the timeline or the send queue, it's time to drop matrix-sdk and
dc609022bb53atagen2026-10-08 12:39 UTC592
matrix-sdk-ui for our own layer over matrix-sdk-base and matrix-sdk-crypto,
dc609022bb53atagen2026-10-08 12:39 UTC593
which keep their public sans-io apis
dc609022bb53atagen2026-10-08 12:39 UTC594
dc609022bb53atagen2026-10-08 12:39 UTC595
- `Room::prepare_encryption`, the send path's encryption prep made public
17e042fbc331atagen2026-10-08 12:39 UTC596
- `SyncServiceBuilder::with_room_list_extra_required_state`, for state the
17e042fbc331atagen2026-10-08 12:39 UTC597
  sdk's hard-coded list leaves out. image packs use it
8fa8a7cde89catagen2026-10-08 12:39 UTC598
- msc4027 reaction shortcodes, kept on the timeline's `ReactionInfo` and sent
8fa8a7cde89catagen2026-10-08 12:39 UTC599
  with `Timeline::toggle_reaction_with_shortcode`
411bc1c78b65atagen2026-10-22 07:39 UTC600
- `SendQueue::enable_global_upload_progress`, upload progress on the
411bc1c78b65atagen2026-10-22 07:39 UTC601
  client-wide send queue channel
d8d8a9fe770aatagen2026-10-22 07:44 UTC602
- `OAuth::pending_authorization` and `restore_pending_authorization`, to
d8d8a9fe770aatagen2026-10-22 07:44 UTC603
  finish an oauth login in another process
dc609022bb53atagen2026-10-08 12:39 UTC604
31ce2453c82fatagen2026-09-24 13:07 UTC605
## public model
31ce2453c82fatagen2026-09-24 13:07 UTC606
31ce2453c82fatagen2026-09-24 13:07 UTC607
owned ruma ids and typed data wherever there's a stable shape, `Raw` where
e9b6478f917fatagen2026-10-08 11:14 UTC608
arbitrary content has to survive. ruma is re-exported as `macltok::ruma`, so a
e9b6478f917fatagen2026-10-08 11:14 UTC609
host parses ids at the version we're typed against. anything expected to grow is
31ce2453c82fatagen2026-09-24 13:07 UTC610
`#[non_exhaustive]`. `Engine::from_client` stays public for people who built
31ce2453c82fatagen2026-09-24 13:07 UTC611
their own client