96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 1 | # macltok design decisions |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 2 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 3 | the contract the working tree actually implements. if the code disagrees with |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 4 | this, one of them is lying and it should be fixed |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 5 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 6 | ## two ways in, one way out |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 7 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 8 | state changes are commands. `ClientHandle::send` queues one and hands back a |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 9 | `Pending`, `execute` queues and waits, and there's an awaited convenience method |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 10 | for every command. each command answers exactly once with |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 11 | `Result<CommandOutput, Error>`. drop the `Pending` if you don't care, the |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 12 | command still runs |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 13 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 14 | pure reads skip the engine and ask the sdk directly through the handle. rooms, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 15 | members, power levels, account data, devices, push notification resolution, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 16 | and media bytes all live here |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 17 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 18 | the event stream carries state and only state. timelines, history, typing, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 19 | rooms, connection, encryption, verification, upload progress, session. no |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 20 | command results ride on it. drain it from its own task, it's bounded and the |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 21 | engine waits for you |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 22 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 23 | one error type for everything, `Error { kind, message, challenge }`. match on |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 24 | `kind`. `Interrupted` means the engine stopped, the room closed, or the task |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 25 | doing the work died before it could answer. `challenge` turns up with |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 26 | `InteractiveAuthRequired` |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 27 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 28 | ## engine |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 29 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 30 | the run loop routes and does nothing slow. every task it spawns is registered |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 31 | with a context. a command task that panics costs its caller an `Interrupted` |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 32 | and gets logged. a watcher or a room actor that panics takes the engine down, |
62fe32759529 | atagen | 2026-09-26 08:50 UTC | 33 | emitting `Failed` then `Stopped`. natively, anyway. wasm aborts the whole |
62fe32759529 | atagen | 2026-09-26 08:50 UTC | 34 | instance on any panic and nothing is left to log it |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 35 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 36 | sync restarts itself with backoff after an error or an unexpected stop, and |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 37 | `ConnectionChanged` shows `Offline` meanwhile. a run that drops again inside |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 38 | thirty seconds counts as a failure too, or a server that answers `/versions` |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 39 | but refuses sync would bounce us between online and offline forever. every return to `Online` re-arms |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 40 | the send queues, since the sdk parks a room's queue on any send failure and |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 41 | never unparks it. a recoverable failure while online gets the room another go |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 42 | after a few seconds |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 43 | |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 44 | logout and a rejected token both stop the engine. `run` resolves to a |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 45 | `Termination` naming which of those it was, or a shutdown, or every handle |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 46 | dropping, so the host never works it out from the events. `Shutdown` answers once |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 47 | `Stopped` has gone out, every one of them if several were queued |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 48 | |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 49 | `Engine::run` hands back a boxed future. the real one nests deep enough that |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 50 | every crate spawning it would trip the compiler's query depth |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 51 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 52 | ## rooms |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 53 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 54 | one actor per open room. the engine holds a slot per room, `Active` with a |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 55 | sender or `Closing` with a fifo of opens and closes that arrived while it wound |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 56 | down. those replay in order once the actor exits. a room you leave, or get |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 57 | kicked or banned from, closes itself. it tells the engine first, and anything |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 58 | already queued to it comes back to be answered or replayed, so an open racing a |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 59 | leave still gets a fresh actor |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 60 | |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 61 | however an actor ends, short of the engine stopping, its typing goes out empty |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 62 | and each of its timelines sends `TimelineClosed`. so does a closed thread. a |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 63 | timeline that never sent a reset never sends a close either |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 64 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 65 | opening sends `TimelineReset` on the stream before it answers. opening an |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 66 | already open room sends a fresh reset, so a second frontend gets a snapshot |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 67 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 68 | sends, edits and replies run on the actor in order, so they reach the send |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 69 | queue in the order they were asked for. redactions, reactions and read |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 70 | receipts are network calls, so they run beside the actor and a slow one |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 71 | doesn't queue the room behind it. closing lets them finish rather than lie |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 72 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 73 | threads belong to their room's actor. commands for a thread that's still |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 74 | opening wait behind the open, up to the actor's queue capacity |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 75 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 76 | pagination runs one request at a time per timeline and emits `HistoryChanged` |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 77 | around each. the sdk remembers where history starts, we don't, because a gappy |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 78 | sync can unload it again |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 79 | |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 80 | the actor's watchers are supervised, and a panic in any of them panics the |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 81 | actor. its command work isn't, a panicking redaction just answers `Interrupted` |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 82 | and gets logged |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 83 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 84 | ## timelines |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 85 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 86 | `TimelineDiff::try_apply` refuses a diff whose indices don't fit your snapshot |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 87 | and leaves the snapshot alone. `try_apply_all` does the same for a whole batch, |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 88 | checking every diff against lengths alone first, since that's all a diff's fit |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 89 | depends on. snapshots are `imbl::Vector`s so keeping one is a pointer copy. |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 90 | stop applying, `resync`, and carry on from the next `TimelineReset`. resync |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 91 | kills the old watcher before the new reset goes out, so no stale diff can land |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 92 | after it |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 93 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 94 | `TimelineId` tells a room from a thread by its root event |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 95 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 96 | every item is an event, a day divider, the read marker or the start. an event |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 97 | carries its sender with avatar and name ambiguity, and a `TimelineContent` |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 98 | saying what it actually is. messages keep their msgtype, membership and profile |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 99 | changes and the common room state changes get their own variants, and anything |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 100 | else is `OtherState` or `Other` with its type. nothing is flattened into english |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 101 | in a body field |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 102 | |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 103 | thread receipts for a thread that isn't open go straight to the room, so |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 104 | marking a thread read works the same whether its room is on screen or not |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 105 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 106 | replies carry the replied-to event once it's loaded. the watcher asks the sdk |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 107 | for any it hasn't got, once per event, since the sdk won't on its own |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 108 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 109 | edit, redact and react take an `EventRef`, so your own message can be edited or |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 110 | reacted to before the server has echoed it back. redacting an unsent one just |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 111 | cancels it |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 112 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 113 | ## mutations |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 114 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 115 | membership, moderation, power levels, room creation and account data writes go |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 116 | through one keyed fifo scheduler. same room, same alias, same dm partner or |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 117 | same account data type run in order, everything else runs at once. |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 118 | `create_direct` hands back the existing dm when there is one |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 119 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 120 | notifications and encryption each get one serial worker. the encryption worker |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 121 | owns the verification controller outright |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 122 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 123 | power level changes build one state event from the local mapping, since the |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 124 | sdk's own patcher is private |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 125 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 126 | ## media |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 127 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 128 | media is a read. `ClientHandle::media` takes a `MediaId` a retained timeline |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 129 | handed you, `mxc_media` takes any plain mxc uri. both share one bounded |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 130 | download path with the byte cap the sdk's own download lacks, an hour's total |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 131 | deadline instead of the sdk client's thirty seconds, and a minute's patience |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 132 | for a stalled stream. concurrent asks for the same bytes share one flight. |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 133 | a thumbnail the server won't make falls back to the file itself. |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 134 | `MediaId` parses from a string, since a web client has to round trip it |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 135 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 136 | the cache holds plaintext, keyed like the sdk keys it, so the sdk and macltok |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 137 | can read each other's entries. that's how your own encrypted uploads come back. |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 138 | the sdk keys plain and encrypted sources alike, so a plain event pointing at |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 139 | someone's encrypted file can leave ciphertext in the cache. an encrypted read |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 140 | checks the cached bytes against the file's hash and decrypts them if they match |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 141 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 142 | media ids are registered as timelines render and forgotten when their timeline |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 143 | closes, least recently seen first once the cap is hit |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 144 | |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 145 | uploads are commands that answer once the send queue has them. progress only |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 146 | goes out on a room's own send queue channel, so each room that uploads gets a |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 147 | forwarder, subscribed before the upload is queued and restarted for uploads the |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 148 | sdk resumes from last time. everything but progress reaches the client-wide |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 149 | channel, so one watcher there follows every attachment in the queue and ends |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 150 | each with one `MediaUploadFinished`. sent, cancelled, its room forgotten, or |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 151 | missed when that channel overflowed at the wrong moment, since then there's no |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 152 | knowing which. a failure isn't an ending, a wedged upload can still be retried. |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 153 | uploads don't belong to a room's actor, so closing the room leaves them be |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 154 | |
7c26fad34822 | atagen | 2026-09-26 09:41 UTC | 155 | the forwarders hand their progress to that watcher rather than the stream, so |
7c26fad34822 | atagen | 2026-09-26 09:41 UTC | 156 | it's the only thing speaking about uploads. anything that ever showed progress |
7c26fad34822 | atagen | 2026-09-26 09:41 UTC | 157 | or a failure is followed until it gets exactly one `MediaUploadFinished`, and |
7c26fad34822 | atagen | 2026-09-26 09:41 UTC | 158 | nothing about it goes out after. it remembers what has ended for as long as it |
7c26fad34822 | atagen | 2026-09-26 09:41 UTC | 159 | runs, since a forwarder can still be sitting on progress for an upload that's |
7c26fad34822 | atagen | 2026-09-26 09:41 UTC | 160 | already sent and nothing says when that backlog is gone |
7c26fad34822 | atagen | 2026-09-26 09:41 UTC | 161 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 162 | ## sessions and secrets |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 163 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 164 | `Session::login_password`, `begin_oauth` and `restore` share one client builder |
62fe32759529 | atagen | 2026-09-26 08:50 UTC | 165 | with an encrypted store, refresh tokens and threads. the store is sqlite |
62fe32759529 | atagen | 2026-09-26 08:50 UTC | 166 | natively and indexeddb in a browser. secret-bearing inputs don't derive `Clone` |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 167 | |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 168 | a `SessionStore` owns one login's record and its store. login saves the first |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 169 | record, `restore` loads it, and the caller saves `ClientHandle::session_record()` |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 170 | on every `SessionTokensChanged`. `EncryptedSessionStore` seals the record with |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 171 | the sdk's `StoreCipher` under the store's passphrase and replaces it by rename, |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 172 | so a crash mid save leaves the old one. it keeps the record out of the sdk's |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 173 | state store because sqlite writes custom values there in the clear, and a |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 174 | client can't open its store before it knows the homeserver the record holds. |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 175 | `discard` deletes the record, then the store, since a record without its store |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 176 | would restore into empty crypto. the host follows `Termination::retain`. |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 177 | `Nothing` discards. `Store` calls `soft_log_out`, which keeps the store and |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 178 | swaps the record for one holding the homeserver, user, device and oauth client |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 179 | id with no tokens. `restore` hands that back as `Restore::SoftLoggedOut`, so a |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 180 | restart still knows which device to log in again as, and the login overwrites |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 181 | it. one store directory per login |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 182 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 183 | ## room lists, spaces and profiles |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 184 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 185 | `RoomSummary` has what a room list row needs without a second query. topic, |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 186 | alias, member counts, heroes for nameless rooms, the inviter, where an upgraded |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 187 | room went, a latest event preview, the joined spaces listing it, and for a |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 188 | space the children it lists |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 189 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 190 | parents come from one lazily built `SpaceService` shared by every handle. |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 191 | children come straight from the space's local state, joined or not. |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 192 | `space_hierarchy` pages through the server's view for browsing rooms you |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 193 | haven't joined, and holds its place between pages |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 194 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 195 | `own_profile` and `user_profile` read profiles. name and avatar changes are |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 196 | commands, and uploading an avatar gets the same byte cap as any upload |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 197 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 198 | ## notifications and room data |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 199 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 200 | the notification client is built lazily and shares the engine's sync service. |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 201 | sdk ui types stay behind the boundary |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 202 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 203 | `rooms` normalises on demand with bounded concurrency and sorts by latest event, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 204 | then name, then id. `RoomsChanged` is an invalidation, coalesced so one sync |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 205 | touching fifty rooms is one event. a left room stays in the list with its |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 206 | membership until it's forgotten. push rule changes never touch room info, so |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 207 | `NotificationSettingsChanged` brings a `RoomsChanged` along for the modes in |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 208 | the summaries |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 209 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 210 | ## verification |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 211 | |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 212 | `Accept`, `Confirm`, `Reject` and `Cancel`. a request another device sends |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 213 | shows up as `Incoming` and waits for `Accept`, or `Cancel` to turn it down. one |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 214 | is only taken up while nothing else is in flight. reject tells the other side |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 215 | the emoji didn't match. the watchers subscribe before they look at the current |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 216 | state, since the sdk only reports what changes after you ask. the watcher and |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 217 | the action paths claim the terminal event, so a racing |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 218 | cancel can't turn `Done` into `Cancelled` |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 219 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 220 | ## public model |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 221 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 222 | owned ruma ids and typed data wherever there's a stable shape, `Raw` where |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 223 | arbitrary content has to survive. anything expected to grow is |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 224 | `#[non_exhaustive]`. `Engine::from_client` stays public for people who built |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 225 | their own client |