atagen

macltok

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

Blame: docs/design-decisions.md

96fdf90aacd6atagen2026-09-08 08:24 UTC1
# macltok design decisions
96fdf90aacd6atagen2026-09-08 08:24 UTC2
31ce2453c82fatagen2026-09-24 13:07 UTC3
the contract the working tree actually implements. if the code disagrees with
31ce2453c82fatagen2026-09-24 13:07 UTC4
this, one of them is lying and it should be fixed
31ce2453c82fatagen2026-09-24 13:07 UTC5
31ce2453c82fatagen2026-09-24 13:07 UTC6
## two ways in, one way out
31ce2453c82fatagen2026-09-24 13:07 UTC7
31ce2453c82fatagen2026-09-24 13:07 UTC8
state changes are commands. `ClientHandle::send` queues one and hands back a
31ce2453c82fatagen2026-09-24 13:07 UTC9
`Pending`, `execute` queues and waits, and there's an awaited convenience method
31ce2453c82fatagen2026-09-24 13:07 UTC10
for every command. each command answers exactly once with
31ce2453c82fatagen2026-09-24 13:07 UTC11
`Result<CommandOutput, Error>`. drop the `Pending` if you don't care, the
31ce2453c82fatagen2026-09-24 13:07 UTC12
command still runs
31ce2453c82fatagen2026-09-24 13:07 UTC13
31ce2453c82fatagen2026-09-24 13:07 UTC14
pure reads skip the engine and ask the sdk directly through the handle. rooms,
31ce2453c82fatagen2026-09-24 13:07 UTC15
members, power levels, account data, devices, push notification resolution,
31ce2453c82fatagen2026-09-24 13:07 UTC16
and media bytes all live here
31ce2453c82fatagen2026-09-24 13:07 UTC17
31ce2453c82fatagen2026-09-24 13:07 UTC18
the event stream carries state and only state. timelines, history, typing,
31ce2453c82fatagen2026-09-24 13:07 UTC19
rooms, connection, encryption, verification, upload progress, session. no
31ce2453c82fatagen2026-09-24 13:07 UTC20
command results ride on it. drain it from its own task, it's bounded and the
31ce2453c82fatagen2026-09-24 13:07 UTC21
engine waits for you
31ce2453c82fatagen2026-09-24 13:07 UTC22
31ce2453c82fatagen2026-09-24 13:07 UTC23
one error type for everything, `Error { kind, message, challenge }`. match on
31ce2453c82fatagen2026-09-24 13:07 UTC24
`kind`. `Interrupted` means the engine stopped, the room closed, or the task
31ce2453c82fatagen2026-09-24 13:07 UTC25
doing the work died before it could answer. `challenge` turns up with
31ce2453c82fatagen2026-09-24 13:07 UTC26
`InteractiveAuthRequired`
31ce2453c82fatagen2026-09-24 13:07 UTC27
31ce2453c82fatagen2026-09-24 13:07 UTC28
## engine
31ce2453c82fatagen2026-09-24 13:07 UTC29
31ce2453c82fatagen2026-09-24 13:07 UTC30
the run loop routes and does nothing slow. every task it spawns is registered
31ce2453c82fatagen2026-09-24 13:07 UTC31
with a context. a command task that panics costs its caller an `Interrupted`
31ce2453c82fatagen2026-09-24 13:07 UTC32
and gets logged. a watcher or a room actor that panics takes the engine down,
7b376d620a3datagen2026-09-26 12:53 UTC33
emitting `Failed` then `Stopped`. natively, anyway. on wasm a panic traps the
7b376d620a3datagen2026-09-26 12:53 UTC34
task mid poll and nothing is caught. the rest keeps running in an undefined
7b376d620a3datagen2026-09-26 12:53 UTC35
state and goes silent, no `Failed` and no `Stopped`, so the host hears of it
7b376d620a3datagen2026-09-26 12:53 UTC36
through `on_panic`, a free function since a panic hook belongs to the whole
7b376d620a3datagen2026-09-26 12:53 UTC37
instance rather than to one engine
31ce2453c82fatagen2026-09-24 13:07 UTC38
31ce2453c82fatagen2026-09-24 13:07 UTC39
sync restarts itself with backoff after an error or an unexpected stop, and
5a52012c9b1datagen2026-09-24 23:45 UTC40
`ConnectionChanged` shows `Offline` meanwhile. a run that drops again inside
5a52012c9b1datagen2026-09-24 23:45 UTC41
thirty seconds counts as a failure too, or a server that answers `/versions`
5a52012c9b1datagen2026-09-24 23:45 UTC42
but refuses sync would bounce us between online and offline forever. every return to `Online` re-arms
31ce2453c82fatagen2026-09-24 13:07 UTC43
the send queues, since the sdk parks a room's queue on any send failure and
31ce2453c82fatagen2026-09-24 13:07 UTC44
never unparks it. a recoverable failure while online gets the room another go
31ce2453c82fatagen2026-09-24 13:07 UTC45
after a few seconds
31ce2453c82fatagen2026-09-24 13:07 UTC46
fe4133dbcd47atagen2026-09-26 09:05 UTC47
logout and a rejected token both stop the engine. `run` resolves to a
fe4133dbcd47atagen2026-09-26 09:05 UTC48
`Termination` naming which of those it was, or a shutdown, or every handle
fe4133dbcd47atagen2026-09-26 09:05 UTC49
dropping, so the host never works it out from the events. `Shutdown` answers once
5a52012c9b1datagen2026-09-24 23:45 UTC50
`Stopped` has gone out, every one of them if several were queued
5a52012c9b1datagen2026-09-24 23:45 UTC51
5a52012c9b1datagen2026-09-24 23:45 UTC52
`Engine::run` hands back a boxed future. the real one nests deep enough that
5a52012c9b1datagen2026-09-24 23:45 UTC53
every crate spawning it would trip the compiler's query depth
31ce2453c82fatagen2026-09-24 13:07 UTC54
31ce2453c82fatagen2026-09-24 13:07 UTC55
## rooms
31ce2453c82fatagen2026-09-24 13:07 UTC56
31ce2453c82fatagen2026-09-24 13:07 UTC57
one actor per open room. the engine holds a slot per room, `Active` with a
31ce2453c82fatagen2026-09-24 13:07 UTC58
sender or `Closing` with a fifo of opens and closes that arrived while it wound
31ce2453c82fatagen2026-09-24 13:07 UTC59
down. those replay in order once the actor exits. a room you leave, or get
5a52012c9b1datagen2026-09-24 23:45 UTC60
kicked or banned from, closes itself. it tells the engine first, and anything
5a52012c9b1datagen2026-09-24 23:45 UTC61
already queued to it comes back to be answered or replayed, so an open racing a
5a52012c9b1datagen2026-09-24 23:45 UTC62
leave still gets a fresh actor
31ce2453c82fatagen2026-09-24 13:07 UTC63
0bfa3f46f0beatagen2026-09-26 09:12 UTC64
however an actor ends, short of the engine stopping, its typing goes out empty
0bfa3f46f0beatagen2026-09-26 09:12 UTC65
and each of its timelines sends `TimelineClosed`. so does a closed thread. a
0bfa3f46f0beatagen2026-09-26 09:12 UTC66
timeline that never sent a reset never sends a close either
0bfa3f46f0beatagen2026-09-26 09:12 UTC67
31ce2453c82fatagen2026-09-24 13:07 UTC68
opening sends `TimelineReset` on the stream before it answers. opening an
31ce2453c82fatagen2026-09-24 13:07 UTC69
already open room sends a fresh reset, so a second frontend gets a snapshot
31ce2453c82fatagen2026-09-24 13:07 UTC70
31ce2453c82fatagen2026-09-24 13:07 UTC71
sends, edits and replies run on the actor in order, so they reach the send
31ce2453c82fatagen2026-09-24 13:07 UTC72
queue in the order they were asked for. redactions, reactions and read
31ce2453c82fatagen2026-09-24 13:07 UTC73
receipts are network calls, so they run beside the actor and a slow one
31ce2453c82fatagen2026-09-24 13:07 UTC74
doesn't queue the room behind it. closing lets them finish rather than lie
31ce2453c82fatagen2026-09-24 13:07 UTC75
31ce2453c82fatagen2026-09-24 13:07 UTC76
threads belong to their room's actor. commands for a thread that's still
31ce2453c82fatagen2026-09-24 13:07 UTC77
opening wait behind the open, up to the actor's queue capacity
31ce2453c82fatagen2026-09-24 13:07 UTC78
31ce2453c82fatagen2026-09-24 13:07 UTC79
pagination runs one request at a time per timeline and emits `HistoryChanged`
31ce2453c82fatagen2026-09-24 13:07 UTC80
around each. the sdk remembers where history starts, we don't, because a gappy
31ce2453c82fatagen2026-09-24 13:07 UTC81
sync can unload it again
31ce2453c82fatagen2026-09-24 13:07 UTC82
5a52012c9b1datagen2026-09-24 23:45 UTC83
the actor's watchers are supervised, and a panic in any of them panics the
5a52012c9b1datagen2026-09-24 23:45 UTC84
actor. its command work isn't, a panicking redaction just answers `Interrupted`
5a52012c9b1datagen2026-09-24 23:45 UTC85
and gets logged
31ce2453c82fatagen2026-09-24 13:07 UTC86
31ce2453c82fatagen2026-09-24 13:07 UTC87
## timelines
31ce2453c82fatagen2026-09-24 13:07 UTC88
31ce2453c82fatagen2026-09-24 13:07 UTC89
`TimelineDiff::try_apply` refuses a diff whose indices don't fit your snapshot
d5d8097c3afdatagen2026-09-26 09:33 UTC90
and leaves the snapshot alone. `try_apply_all` does the same for a whole batch,
d5d8097c3afdatagen2026-09-26 09:33 UTC91
checking every diff against lengths alone first, since that's all a diff's fit
d5d8097c3afdatagen2026-09-26 09:33 UTC92
depends on. snapshots are `imbl::Vector`s so keeping one is a pointer copy.
d5d8097c3afdatagen2026-09-26 09:33 UTC93
stop applying, `resync`, and carry on from the next `TimelineReset`. resync
d5d8097c3afdatagen2026-09-26 09:33 UTC94
kills the old watcher before the new reset goes out, so no stale diff can land
d5d8097c3afdatagen2026-09-26 09:33 UTC95
after it
31ce2453c82fatagen2026-09-24 13:07 UTC96
31ce2453c82fatagen2026-09-24 13:07 UTC97
`TimelineId` tells a room from a thread by its root event
31ce2453c82fatagen2026-09-24 13:07 UTC98
960ac0721079atagen2026-09-25 00:24 UTC99
every item is an event, a day divider, the read marker or the start. an event
960ac0721079atagen2026-09-25 00:24 UTC100
carries its sender with avatar and name ambiguity, and a `TimelineContent`
960ac0721079atagen2026-09-25 00:24 UTC101
saying what it actually is. messages keep their msgtype, membership and profile
960ac0721079atagen2026-09-25 00:24 UTC102
changes and the common room state changes get their own variants, and anything
960ac0721079atagen2026-09-25 00:24 UTC103
else is `OtherState` or `Other` with its type. nothing is flattened into english
960ac0721079atagen2026-09-25 00:24 UTC104
in a body field
960ac0721079atagen2026-09-25 00:24 UTC105
5a52012c9b1datagen2026-09-24 23:45 UTC106
thread receipts for a thread that isn't open go straight to the room, so
5a52012c9b1datagen2026-09-24 23:45 UTC107
marking a thread read works the same whether its room is on screen or not
5a52012c9b1datagen2026-09-24 23:45 UTC108
960ac0721079atagen2026-09-25 00:24 UTC109
replies carry the replied-to event once it's loaded. the watcher asks the sdk
960ac0721079atagen2026-09-25 00:24 UTC110
for any it hasn't got, once per event, since the sdk won't on its own
960ac0721079atagen2026-09-25 00:24 UTC111
960ac0721079atagen2026-09-25 00:24 UTC112
edit, redact and react take an `EventRef`, so your own message can be edited or
960ac0721079atagen2026-09-25 00:24 UTC113
reacted to before the server has echoed it back. redacting an unsent one just
960ac0721079atagen2026-09-25 00:24 UTC114
cancels it
960ac0721079atagen2026-09-25 00:24 UTC115
31ce2453c82fatagen2026-09-24 13:07 UTC116
## mutations
31ce2453c82fatagen2026-09-24 13:07 UTC117
31ce2453c82fatagen2026-09-24 13:07 UTC118
membership, moderation, power levels, room creation and account data writes go
31ce2453c82fatagen2026-09-24 13:07 UTC119
through one keyed fifo scheduler. same room, same alias, same dm partner or
31ce2453c82fatagen2026-09-24 13:07 UTC120
same account data type run in order, everything else runs at once.
31ce2453c82fatagen2026-09-24 13:07 UTC121
`create_direct` hands back the existing dm when there is one
31ce2453c82fatagen2026-09-24 13:07 UTC122
31ce2453c82fatagen2026-09-24 13:07 UTC123
notifications and encryption each get one serial worker. the encryption worker
31ce2453c82fatagen2026-09-24 13:07 UTC124
owns the verification controller outright
31ce2453c82fatagen2026-09-24 13:07 UTC125
31ce2453c82fatagen2026-09-24 13:07 UTC126
power level changes build one state event from the local mapping, since the
31ce2453c82fatagen2026-09-24 13:07 UTC127
sdk's own patcher is private
31ce2453c82fatagen2026-09-24 13:07 UTC128
31ce2453c82fatagen2026-09-24 13:07 UTC129
## media
31ce2453c82fatagen2026-09-24 13:07 UTC130
31ce2453c82fatagen2026-09-24 13:07 UTC131
media is a read. `ClientHandle::media` takes a `MediaId` a retained timeline
31ce2453c82fatagen2026-09-24 13:07 UTC132
handed you, `mxc_media` takes any plain mxc uri. both share one bounded
5a52012c9b1datagen2026-09-24 23:45 UTC133
download path with the byte cap the sdk's own download lacks, an hour's total
5a52012c9b1datagen2026-09-24 23:45 UTC134
deadline instead of the sdk client's thirty seconds, and a minute's patience
5a52012c9b1datagen2026-09-24 23:45 UTC135
for a stalled stream. concurrent asks for the same bytes share one flight.
5a52012c9b1datagen2026-09-24 23:45 UTC136
a thumbnail the server won't make falls back to the file itself.
5a52012c9b1datagen2026-09-24 23:45 UTC137
`MediaId` parses from a string, since a web client has to round trip it
31ce2453c82fatagen2026-09-24 13:07 UTC138
31ce2453c82fatagen2026-09-24 13:07 UTC139
the cache holds plaintext, keyed like the sdk keys it, so the sdk and macltok
5a52012c9b1datagen2026-09-24 23:45 UTC140
can read each other's entries. that's how your own encrypted uploads come back.
5a52012c9b1datagen2026-09-24 23:45 UTC141
the sdk keys plain and encrypted sources alike, so a plain event pointing at
5a52012c9b1datagen2026-09-24 23:45 UTC142
someone's encrypted file can leave ciphertext in the cache. an encrypted read
5a52012c9b1datagen2026-09-24 23:45 UTC143
checks the cached bytes against the file's hash and decrypts them if they match
31ce2453c82fatagen2026-09-24 13:07 UTC144
31ce2453c82fatagen2026-09-24 13:07 UTC145
media ids are registered as timelines render and forgotten when their timeline
31ce2453c82fatagen2026-09-24 13:07 UTC146
closes, least recently seen first once the cap is hit
31ce2453c82fatagen2026-09-24 13:07 UTC147
5a52012c9b1datagen2026-09-24 23:45 UTC148
uploads are commands that answer once the send queue has them. progress only
5a52012c9b1datagen2026-09-24 23:45 UTC149
goes out on a room's own send queue channel, so each room that uploads gets a
5a52012c9b1datagen2026-09-24 23:45 UTC150
forwarder, subscribed before the upload is queued and restarted for uploads the
0bfa3f46f0beatagen2026-09-26 09:12 UTC151
sdk resumes from last time. everything but progress reaches the client-wide
0bfa3f46f0beatagen2026-09-26 09:12 UTC152
channel, so one watcher there follows every attachment in the queue and ends
0bfa3f46f0beatagen2026-09-26 09:12 UTC153
each with one `MediaUploadFinished`. sent, cancelled, its room forgotten, or
0bfa3f46f0beatagen2026-09-26 09:12 UTC154
missed when that channel overflowed at the wrong moment, since then there's no
0bfa3f46f0beatagen2026-09-26 09:12 UTC155
knowing which. a failure isn't an ending, a wedged upload can still be retried.
0bfa3f46f0beatagen2026-09-26 09:12 UTC156
uploads don't belong to a room's actor, so closing the room leaves them be
31ce2453c82fatagen2026-09-24 13:07 UTC157
7c26fad34822atagen2026-09-26 09:41 UTC158
the forwarders hand their progress to that watcher rather than the stream, so
7c26fad34822atagen2026-09-26 09:41 UTC159
it's the only thing speaking about uploads. anything that ever showed progress
7c26fad34822atagen2026-09-26 09:41 UTC160
or a failure is followed until it gets exactly one `MediaUploadFinished`, and
7cf348521204atagen2026-09-26 12:28 UTC161
nothing about it goes out after. each forwarder relays its room's sends and
7cf348521204atagen2026-09-26 12:28 UTC162
cancels behind its progress, so an end never overtakes the ticks before it. an
7cf348521204atagen2026-09-26 12:28 UTC163
upload ended from anywhere else is remembered until its forwarder's own end
7cf348521204atagen2026-09-26 12:28 UTC164
turns up, since until then there may be progress still queued for it. the
7cf348521204atagen2026-09-26 12:28 UTC165
engine stopping ends nothing, `Stopped` covers it
7c26fad34822atagen2026-09-26 09:41 UTC166
31ce2453c82fatagen2026-09-24 13:07 UTC167
## sessions and secrets
31ce2453c82fatagen2026-09-24 13:07 UTC168
31ce2453c82fatagen2026-09-24 13:07 UTC169
`Session::login_password`, `begin_oauth` and `restore` share one client builder
62fe32759529atagen2026-09-26 08:50 UTC170
with an encrypted store, refresh tokens and threads. the store is sqlite
62fe32759529atagen2026-09-26 08:50 UTC171
natively and indexeddb in a browser. secret-bearing inputs don't derive `Clone`
31ce2453c82fatagen2026-09-24 13:07 UTC172
713205a95af2atagen2026-09-26 09:24 UTC173
a `SessionStore` owns one login's record and its store. login saves the first
f12377bce9b9atagen2026-09-26 12:32 UTC174
record, `restore` loads it, and the engine a `Session` builds saves every token
f12377bce9b9atagen2026-09-26 12:32 UTC175
rotation after that, since a refresh token works once and a host that missed one
f12377bce9b9atagen2026-09-26 12:32 UTC176
save has lost the login. the session changes are subscribed the moment the
f12377bce9b9atagen2026-09-26 12:32 UTC177
client exists, because the sdk can refresh during restore before any engine
f12377bce9b9atagen2026-09-26 12:32 UTC178
does. a save runs off the event stream, so a full stream never holds it back,
f12377bce9b9atagen2026-09-26 12:32 UTC179
and `SessionTokensChanged` only goes out once it has landed, or
f12377bce9b9atagen2026-09-26 12:32 UTC180
`SessionSaveFailed` if it didn't. natively the sdk's save callback runs inside
f12377bce9b9atagen2026-09-26 12:32 UTC181
the refresh, before the new token is used, and `save_blocking` writes there and
f12377bce9b9atagen2026-09-26 12:32 UTC182
then. in a browser there's only the async save, so a tab that dies between a
f12377bce9b9atagen2026-09-26 12:32 UTC183
refresh and its save wakes up holding a spent token. `without_persistence`
f12377bce9b9atagen2026-09-26 12:32 UTC184
hands the job back to the host. `EncryptedSessionStore` seals the record with
713205a95af2atagen2026-09-26 09:24 UTC185
the sdk's `StoreCipher` under the store's passphrase and replaces it by rename,
713205a95af2atagen2026-09-26 09:24 UTC186
so a crash mid save leaves the old one. it keeps the record out of the sdk's
713205a95af2atagen2026-09-26 09:24 UTC187
state store because sqlite writes custom values there in the clear, and a
713205a95af2atagen2026-09-26 09:24 UTC188
client can't open its store before it knows the homeserver the record holds.
713205a95af2atagen2026-09-26 09:24 UTC189
`discard` deletes the record, then the store, since a record without its store
6f1d6591c10datagen2026-09-26 09:52 UTC190
would restore into empty crypto. the host follows `Termination::retain`.
6f1d6591c10datagen2026-09-26 09:52 UTC191
`Nothing` discards. `Store` calls `soft_log_out`, which keeps the store and
6f1d6591c10datagen2026-09-26 09:52 UTC192
swaps the record for one holding the homeserver, user, device and oauth client
6f1d6591c10datagen2026-09-26 09:52 UTC193
id with no tokens. `restore` hands that back as `Restore::SoftLoggedOut`, so a
6f1d6591c10datagen2026-09-26 09:52 UTC194
restart still knows which device to log in again as, and the login overwrites
6f1d6591c10datagen2026-09-26 09:52 UTC195
it. one store directory per login
31ce2453c82fatagen2026-09-24 13:07 UTC196
960ac0721079atagen2026-09-25 00:24 UTC197
## room lists, spaces and profiles
960ac0721079atagen2026-09-25 00:24 UTC198
960ac0721079atagen2026-09-25 00:24 UTC199
`RoomSummary` has what a room list row needs without a second query. topic,
960ac0721079atagen2026-09-25 00:24 UTC200
alias, member counts, heroes for nameless rooms, the inviter, where an upgraded
960ac0721079atagen2026-09-25 00:24 UTC201
room went, a latest event preview, the joined spaces listing it, and for a
960ac0721079atagen2026-09-25 00:24 UTC202
space the children it lists
960ac0721079atagen2026-09-25 00:24 UTC203
960ac0721079atagen2026-09-25 00:24 UTC204
parents come from one lazily built `SpaceService` shared by every handle.
960ac0721079atagen2026-09-25 00:24 UTC205
children come straight from the space's local state, joined or not.
960ac0721079atagen2026-09-25 00:24 UTC206
`space_hierarchy` pages through the server's view for browsing rooms you
960ac0721079atagen2026-09-25 00:24 UTC207
haven't joined, and holds its place between pages
960ac0721079atagen2026-09-25 00:24 UTC208
960ac0721079atagen2026-09-25 00:24 UTC209
`own_profile` and `user_profile` read profiles. name and avatar changes are
960ac0721079atagen2026-09-25 00:24 UTC210
commands, and uploading an avatar gets the same byte cap as any upload
960ac0721079atagen2026-09-25 00:24 UTC211
31ce2453c82fatagen2026-09-24 13:07 UTC212
## notifications and room data
31ce2453c82fatagen2026-09-24 13:07 UTC213
31ce2453c82fatagen2026-09-24 13:07 UTC214
the notification client is built lazily and shares the engine's sync service.
31ce2453c82fatagen2026-09-24 13:07 UTC215
sdk ui types stay behind the boundary
31ce2453c82fatagen2026-09-24 13:07 UTC216
31ce2453c82fatagen2026-09-24 13:07 UTC217
`rooms` normalises on demand with bounded concurrency and sorts by latest event,
31ce2453c82fatagen2026-09-24 13:07 UTC218
then name, then id. `RoomsChanged` is an invalidation, coalesced so one sync
0bfa3f46f0beatagen2026-09-26 09:12 UTC219
touching fifty rooms is one event. a left room stays in the list with its
0bfa3f46f0beatagen2026-09-26 09:12 UTC220
membership until it's forgotten. push rule changes never touch room info, so
0bfa3f46f0beatagen2026-09-26 09:12 UTC221
`NotificationSettingsChanged` brings a `RoomsChanged` along for the modes in
0bfa3f46f0beatagen2026-09-26 09:12 UTC222
the summaries
31ce2453c82fatagen2026-09-24 13:07 UTC223
31ce2453c82fatagen2026-09-24 13:07 UTC224
## verification
31ce2453c82fatagen2026-09-24 13:07 UTC225
5a52012c9b1datagen2026-09-24 23:45 UTC226
`Accept`, `Confirm`, `Reject` and `Cancel`. a request another device sends
5a52012c9b1datagen2026-09-24 23:45 UTC227
shows up as `Incoming` and waits for `Accept`, or `Cancel` to turn it down. one
5a52012c9b1datagen2026-09-24 23:45 UTC228
is only taken up while nothing else is in flight. reject tells the other side
5a52012c9b1datagen2026-09-24 23:45 UTC229
the emoji didn't match. the watchers subscribe before they look at the current
5a52012c9b1datagen2026-09-24 23:45 UTC230
state, since the sdk only reports what changes after you ask. the watcher and
5a52012c9b1datagen2026-09-24 23:45 UTC231
the action paths claim the terminal event, so a racing
31ce2453c82fatagen2026-09-24 13:07 UTC232
cancel can't turn `Done` into `Cancelled`
31ce2453c82fatagen2026-09-24 13:07 UTC233
a319bae745d8atagen2026-09-26 09:59 UTC234
## mirror
a319bae745d8atagen2026-09-26 09:59 UTC235
a319bae745d8atagen2026-09-26 09:59 UTC236
the mirror is a consumer like any other, behind the `mirror` feature, and it
a319bae745d8atagen2026-09-26 09:59 UTC237
takes any stream of events rather than the `EventStream` itself so a host keeps
a319bae745d8atagen2026-09-26 09:59 UTC238
the raw events without a second channel. state, its sequence number and the
a319bae745d8atagen2026-09-26 09:59 UTC239
broadcast send all move under one lock, which is how `subscribe` gets a
a319bae745d8atagen2026-09-26 09:59 UTC240
snapshot and a receiver with nothing between them. a lagged subscriber starts
a319bae745d8atagen2026-09-26 09:59 UTC241
over from a fresh state under the same lock
a319bae745d8atagen2026-09-26 09:59 UTC242
a319bae745d8atagen2026-09-26 09:59 UTC243
nothing on the draining path waits for an engine answer. a resync runs on its
a319bae745d8atagen2026-09-26 09:59 UTC244
own task, retrying with backoff until a reset or a close for its timeline
a319bae745d8atagen2026-09-26 09:59 UTC245
aborts it. rooms are fetched on a task too, one at a time with one more owed at
a319bae745d8atagen2026-09-26 09:59 UTC246
most. the only thing it does wait for is `SessionStore::save`, since a refresh
a319bae745d8atagen2026-09-26 09:59 UTC247
token spent before the save lands loses the login. `SessionTokensChanged`
a319bae745d8atagen2026-09-26 09:59 UTC248
never becomes an update, a lagging subscriber would lose it
a319bae745d8atagen2026-09-26 09:59 UTC249
31ce2453c82fatagen2026-09-24 13:07 UTC250
## public model
31ce2453c82fatagen2026-09-24 13:07 UTC251
31ce2453c82fatagen2026-09-24 13:07 UTC252
owned ruma ids and typed data wherever there's a stable shape, `Raw` where
31ce2453c82fatagen2026-09-24 13:07 UTC253
arbitrary content has to survive. anything expected to grow is
31ce2453c82fatagen2026-09-24 13:07 UTC254
`#[non_exhaustive]`. `Engine::from_client` stays public for people who built
31ce2453c82fatagen2026-09-24 13:07 UTC255
their own client