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 | |
9a25b6ce66f4 | atagen | 2026-10-08 11:15 UTC | 23 | one error type for everything, `Error { kind, message, challenge, server }`. |
9a25b6ce66f4 | atagen | 2026-10-08 11:15 UTC | 24 | match on `kind`. `Interrupted` means the engine stopped, the room closed, or the |
9a25b6ce66f4 | atagen | 2026-10-08 11:15 UTC | 25 | task 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 | |
9a25b6ce66f4 | atagen | 2026-10-08 11:15 UTC | 28 | `message` is ours and says what we were doing. `server` is the homeserver's |
9a25b6ce66f4 | atagen | 2026-10-08 11:15 UTC | 29 | errcode and message, dug out of the source chain when the error is built, so a |
9a25b6ce66f4 | atagen | 2026-10-08 11:15 UTC | 30 | host can show "invalid password" without walking the chain. `AccountError` |
9a25b6ce66f4 | atagen | 2026-10-08 11:15 UTC | 31 | carries the same, plus a `kind`. neither folds it into `Display`, since anything |
9a25b6ce66f4 | atagen | 2026-10-08 11:15 UTC | 32 | printing the chain would say it twice |
9a25b6ce66f4 | atagen | 2026-10-08 11:15 UTC | 33 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 34 | ## engine |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 35 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 36 | the run loop routes and does nothing slow. every task it spawns is registered |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 37 | with a context. a command task that panics costs its caller an `Interrupted` |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 38 | and gets logged. a watcher or a room actor that panics takes the engine down, |
7b376d620a3d | atagen | 2026-09-26 12:53 UTC | 39 | emitting `Failed` then `Stopped`. natively, anyway. on wasm a panic traps the |
7b376d620a3d | atagen | 2026-09-26 12:53 UTC | 40 | task mid poll and nothing is caught. the rest keeps running in an undefined |
7b376d620a3d | atagen | 2026-09-26 12:53 UTC | 41 | state and goes silent, no `Failed` and no `Stopped`, so the host hears of it |
7b376d620a3d | atagen | 2026-09-26 12:53 UTC | 42 | through `on_panic`, a free function since a panic hook belongs to the whole |
7b376d620a3d | atagen | 2026-09-26 12:53 UTC | 43 | instance rather than to one engine |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 44 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 45 | sync restarts itself with backoff after an error or an unexpected stop, and |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 46 | `ConnectionChanged` shows `Offline` meanwhile. a run that drops again inside |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 47 | thirty seconds counts as a failure too, or a server that answers `/versions` |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 48 | 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 | 49 | the send queues, since the sdk parks a room's queue on any send failure and |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 50 | never unparks it. a recoverable failure while online gets the room another go |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 51 | after a few seconds |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 52 | |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 53 | logout and a rejected token both stop the engine. `run` resolves to a |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 54 | `Termination` naming which of those it was, or a shutdown, or every handle |
fe4133dbcd47 | atagen | 2026-09-26 09:05 UTC | 55 | dropping, so the host never works it out from the events. `Shutdown` answers once |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 56 | `Stopped` has gone out, every one of them if several were queued |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 57 | |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 58 | `Engine::run` hands back a boxed future. the real one nests deep enough that |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 59 | every crate spawning it would trip the compiler's query depth |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 60 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 61 | ## rooms |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 62 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 63 | one actor per open room. the engine holds a slot per room, `Active` with a |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 64 | sender or `Closing` with a fifo of opens and closes that arrived while it wound |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 65 | down. those replay in order once the actor exits. a room you leave, or get |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 66 | kicked or banned from, closes itself. it tells the engine first, and anything |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 67 | already queued to it comes back to be answered or replayed, so an open racing a |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 68 | leave still gets a fresh actor |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 69 | |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 70 | however an actor ends, short of the engine stopping, its typing goes out empty |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 71 | and each of its timelines sends `TimelineClosed`. so does a closed thread. a |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 72 | timeline that never sent a reset never sends a close either |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 73 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 74 | opening sends `TimelineReset` on the stream before it answers. opening an |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 75 | already open room sends a fresh reset, so a second frontend gets a snapshot |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 76 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 77 | sends, edits and replies run on the actor in order, so they reach the send |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 78 | queue in the order they were asked for. redactions, reactions and read |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 79 | receipts are network calls, so they run beside the actor and a slow one |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 80 | doesn't queue the room behind it. closing lets them finish rather than lie |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 81 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 82 | threads belong to their room's actor. commands for a thread that's still |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 83 | opening wait behind the open, up to the actor's queue capacity |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 84 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 85 | pagination runs one request at a time per timeline and emits `HistoryChanged` |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 86 | around each. the sdk remembers where history starts, we don't, because a gappy |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 87 | sync can unload it again |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 88 | |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 89 | the actor's watchers are supervised, and a panic in any of them panics the |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 90 | actor. its command work isn't, a panicking redaction just answers `Interrupted` |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 91 | and gets logged |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 92 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 93 | ## timelines |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 94 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 95 | `TimelineDiff::try_apply` refuses a diff whose indices don't fit your snapshot |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 96 | and leaves the snapshot alone. `try_apply_all` does the same for a whole batch, |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 97 | checking every diff against lengths alone first, since that's all a diff's fit |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 98 | depends on. snapshots are `imbl::Vector`s so keeping one is a pointer copy. |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 99 | stop applying, `resync`, and carry on from the next `TimelineReset`. resync |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 100 | kills the old watcher before the new reset goes out, so no stale diff can land |
d5d8097c3afd | atagen | 2026-09-26 09:33 UTC | 101 | after it |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 102 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 103 | `TimelineId` tells a room from a thread by its root event |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 104 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 105 | every item is an event, a day divider, the read marker or the start. an event |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 106 | carries its sender with avatar and name ambiguity, and a `TimelineContent` |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 107 | saying what it actually is. messages keep their msgtype, membership and profile |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 108 | changes and the common room state changes get their own variants, and anything |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 109 | else is `OtherState` or `Other` with its type. nothing is flattened into english |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 110 | in a body field |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 111 | |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 112 | thread receipts for a thread that isn't open go straight to the room, so |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 113 | marking a thread read works the same whether its room is on screen or not |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 114 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 115 | replies carry the replied-to event once it's loaded. the watcher asks the sdk |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 116 | 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 | 117 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 118 | edit, redact and react take an `EventRef`, so your own message can be edited or |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 119 | reacted to before the server has echoed it back. redacting an unsent one just |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 120 | cancels it |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 121 | |
8fa8a7cde89c | atagen | 2026-10-08 12:39 UTC | 122 | a custom emote reaction keys on the image's mxc uri and carries its shortcode |
8fa8a7cde89c | atagen | 2026-10-08 12:39 UTC | 123 | (msc4027), so clients without the pack can still label it. `Reaction` says |
8fa8a7cde89c | atagen | 2026-10-08 12:39 UTC | 124 | which image it is and the shortcode any sender gave, and `toggle_reaction` |
8fa8a7cde89c | atagen | 2026-10-08 12:39 UTC | 125 | takes the shortcode to send |
8fa8a7cde89c | atagen | 2026-10-08 12:39 UTC | 126 | |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 127 | ## thread lists |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 128 | |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 129 | a room's thread list is its own thing, not a timeline and not the room actor's. |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 130 | it needs only the room, so a room needn't be open for its list. one worker holds |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 131 | every open list. `Open` sends an empty `ThreadListReset`, `Paginate` asks |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 132 | `/rooms/{id}/threads` through the sdk's `Room::list_threads`, and the list |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 133 | follows the room's event cache for as long as it's open. changes go out as |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 134 | upserts keyed by root, which `ThreadListEntry::upsert` puts in order, newest |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 135 | activity first and ties by root, so a list can't diverge the way an index diff |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 136 | can |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 137 | |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 138 | the sdk's `ThreadListService` was passed over. it never adds a thread that wasn't |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 139 | on a page already loaded, never reorders, and adds one to the count for every |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 140 | event the cache hands it, replays and back pagination included. here a reply |
e7c1b037154d | atagen | 2026-10-25 13:50 UTC | 141 | counts once, by id, when sync just brought it, it's newer than the thread's |
e7c1b037154d | atagen | 2026-10-25 13:50 UTC | 142 | latest and it was sent after the count was read off the server, all by the sdk's |
e7c1b037154d | atagen | 2026-10-25 13:50 UTC | 143 | corrected timestamp. the latest alone can't date a count. a summary from the |
e7c1b037154d | atagen | 2026-10-25 13:50 UTC | 144 | event cache names its latest reply without the event, and continuwuity 26.9 |
e7c1b037154d | atagen | 2026-10-25 13:50 UTC | 145 | bundles the latest as bare content, so with only that check every reply the |
e7c1b037154d | atagen | 2026-10-25 13:50 UTC | 146 | cache re-inserted read as new and a page's count doubled. a newer reply that |
97fb5720d881 | atagen | 2026-10-25 14:15 UTC | 147 | doesn't count still becomes the latest, and an entry built without a dated |
97fb5720d881 | atagen | 2026-10-25 14:15 UTC | 148 | latest takes the newest reply the event cache holds. replays, reloads from the store and |
e7c1b037154d | atagen | 2026-10-25 13:50 UTC | 149 | back pagination never count. a reply sync brings to a thread the list hasn't |
e7c1b037154d | atagen | 2026-10-25 13:50 UTC | 150 | got fetches its root, four at a time, and adds it. one from anywhere else |
e7c1b037154d | atagen | 2026-10-25 13:50 UTC | 151 | doesn't, since that |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 152 | says nothing about the thread being active, and a root the server doesn't have |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 153 | isn't asked for again. a redacted reply doesn't uncount, an edit doesn't change |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 154 | the latest reply's preview, and a preview's own reply target comes back |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 155 | `Unavailable` rather than fetched |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 156 | |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 157 | a lagged listener can't know what it missed, so the list fetches its first page |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 158 | again and sends a `ThreadListReset` with it. leaving the room, or being kicked |
60454002af5f | atagen | 2026-10-25 11:13 UTC | 159 | or banned, closes the list with `ThreadListClosed` and `RoomLeft`, the shape an |
60454002af5f | atagen | 2026-10-25 11:13 UTC | 160 | event context closing on a leave has. `Close` sends one too, with `Requested`, |
60454002af5f | atagen | 2026-10-25 11:13 UTC | 161 | since a mirror holds the list and has to drop it. a list's media belongs to its |
60454002af5f | atagen | 2026-10-25 11:13 UTC | 162 | own scope and its ids carry it, like an event context's, bounded the same way so |
60454002af5f | atagen | 2026-10-25 11:13 UTC | 163 | a long open list sheds the previews its threads moved past, and released on |
60454002af5f | atagen | 2026-10-25 11:13 UTC | 164 | close |
66db1b3cd895 | atagen | 2026-10-25 10:28 UTC | 165 | |
38c4caecdb3d | atagen | 2026-09-26 13:35 UTC | 166 | ## warming up to send |
38c4caecdb3d | atagen | 2026-09-26 13:35 UTC | 167 | |
38c4caecdb3d | atagen | 2026-09-26 13:35 UTC | 168 | the sdk does all its key work on the send path, in the room's send queue before |
38c4caecdb3d | atagen | 2026-09-26 13:35 UTC | 169 | the `/send` itself. members synced, a `/keys/query` for anyone untracked or |
38c4caecdb3d | atagen | 2026-09-26 13:35 UTC | 170 | dirty, then a `/keys/claim` and the room key sent to every device. over a slow |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 171 | federation that's a minute of nothing. `prepare_to_send` runs all of it ahead |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 172 | of time through the fork's `Room::prepare_encryption`, the same function the |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 173 | send queue calls. the share sits behind the sdk's per-room dedup, so a send |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 174 | landing mid-warm-up waits for it rather than sharing again, and a send after it |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 175 | finds nothing left to do but the `/send`. a room whose encryption isn't known |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 176 | yet gets asked, which the send would have done anyway. one warm-up per room at |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 177 | a time, a second ask joins it. opening a room or starting to type runs one too, |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 178 | unless `EngineConfig::prepare_to_send` is off. it's on because malice asked, |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 179 | not because anyone decided |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 180 | |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 181 | the session rotates after 100 messages or a week, and sharing isn't a message, |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 182 | so a warm-up is only wasted on a room left idle about that long |
38c4caecdb3d | atagen | 2026-09-26 13:35 UTC | 183 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 184 | ## mutations |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 185 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 186 | membership, moderation, power levels, room creation and account data writes go |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 187 | through one keyed fifo scheduler. same room, same alias, same dm partner or |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 188 | same account data type run in order, everything else runs at once. |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 189 | `create_direct` hands back the existing dm when there is one |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 190 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 191 | notifications and encryption each get one serial worker. the encryption worker |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 192 | owns the verification controller outright |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 193 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 194 | power level changes build one state event from the local mapping, since the |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 195 | sdk's own patcher is private |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 196 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 197 | ## media |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 198 | |
5d2974de5826 | atagen | 2026-10-08 11:29 UTC | 199 | media is a read. `ClientHandle::media` takes a `MediaId`, from a retained |
5d2974de5826 | atagen | 2026-10-08 11:29 UTC | 200 | timeline or an avatar, through one bounded |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 201 | download path with the byte cap the sdk's own download lacks, an hour's total |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 202 | deadline instead of the sdk client's thirty seconds, and a minute's patience |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 203 | for a stalled stream. concurrent asks for the same bytes share one flight. |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 204 | a thumbnail the server won't make falls back to the file itself. |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 205 | `MediaId` parses from a string, since a web client has to round trip it |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 206 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 207 | the cache holds plaintext, keyed like the sdk keys it, so the sdk and macltok |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 208 | can read each other's entries. that's how your own encrypted uploads come back. |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 209 | the sdk keys plain and encrypted sources alike, so a plain event pointing at |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 210 | someone's encrypted file can leave ciphertext in the cache. an encrypted read |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 211 | checks the cached bytes against the file's hash and decrypts them if they match |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 212 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 213 | media ids are registered as timelines render and forgotten when their timeline |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 214 | closes, least recently seen first once the cap is hit |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 215 | |
5d2974de5826 | atagen | 2026-10-08 11:29 UTC | 216 | avatars are `MediaId`s too, so a host never handles an mxc uri. they aren't |
5d2974de5826 | atagen | 2026-10-08 11:29 UTC | 217 | registered, since they live in the room list and member lists for the whole |
5d2974de5826 | atagen | 2026-10-08 11:29 UTC | 218 | session and there can be thousands, and the cap would evict ones still on |
5d2974de5826 | atagen | 2026-10-08 11:29 UTC | 219 | screen. a plain avatar carries no keys, so its id is its mxc uri and resolves |
5d2974de5826 | atagen | 2026-10-08 11:29 UTC | 220 | without the registry. it never expires, and an mxc uri a host got some other way |
5d2974de5826 | atagen | 2026-10-08 11:29 UTC | 221 | parses into one. `set_avatar` still takes an mxc uri |
5d2974de5826 | atagen | 2026-10-08 11:29 UTC | 222 | |
5b21b919d1e6 | atagen | 2026-09-27 11:47 UTC | 223 | streaming decryption inside macltok was declined. in a browser the service |
5b21b919d1e6 | atagen | 2026-09-27 11:47 UTC | 224 | worker answering range requests is a separate context from the worker running |
5b21b919d1e6 | atagen | 2026-09-27 11:47 UTC | 225 | macltok, and streaming through macltok would carry every chunk through wasm |
5b21b919d1e6 | atagen | 2026-09-27 11:47 UTC | 226 | memory, the very thing the host streams to avoid. `media_source` hands over the |
5b21b919d1e6 | atagen | 2026-09-27 11:47 UTC | 227 | url and keys `media` would have used, picked the same way, and `access_token` |
5b21b919d1e6 | atagen | 2026-09-27 11:47 UTC | 228 | the rest. the byte cap doesn't apply, nothing is buffered |
5b21b919d1e6 | atagen | 2026-09-27 11:47 UTC | 229 | |
411bc1c78b65 | atagen | 2026-10-22 07:39 UTC | 230 | uploads are commands that answer once the send queue has them. the fork puts |
411bc1c78b65 | atagen | 2026-10-22 07:39 UTC | 231 | upload progress on the client-wide send queue channel too, which upstream keeps |
411bc1c78b65 | atagen | 2026-10-22 07:39 UTC | 232 | for each room's own, so one watcher there follows every attachment in the queue, |
411bc1c78b65 | atagen | 2026-10-22 07:39 UTC | 233 | leftovers the sdk resumes from last time included, and ends each with one |
411bc1c78b65 | atagen | 2026-10-22 07:39 UTC | 234 | `MediaUploadFinished`. sent, cancelled, its room forgotten, or |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 235 | missed when that channel overflowed at the wrong moment, since then there's no |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 236 | knowing which. a failure isn't an ending, a wedged upload can still be retried. |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 237 | uploads don't belong to a room's actor, so closing the room leaves them be |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 238 | |
411bc1c78b65 | atagen | 2026-10-22 07:39 UTC | 239 | that watcher is the only thing speaking about uploads. anything that ever |
411bc1c78b65 | atagen | 2026-10-22 07:39 UTC | 240 | showed progress or a failure is followed until it gets exactly one |
411bc1c78b65 | atagen | 2026-10-22 07:39 UTC | 241 | `MediaUploadFinished`, and nothing about it goes out after. the sdk sends |
411bc1c78b65 | atagen | 2026-10-22 07:39 UTC | 242 | progress from a detached task, so a tick can turn up behind its own upload's |
411bc1c78b65 | atagen | 2026-10-22 07:39 UTC | 243 | end, and the watcher remembers the last few hundred ends to drop those. the |
7cf348521204 | atagen | 2026-09-26 12:28 UTC | 244 | engine stopping ends nothing, `Stopped` covers it |
7c26fad34822 | atagen | 2026-09-26 09:41 UTC | 245 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 246 | ## sessions and secrets |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 247 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 248 | `Session::login_password`, `begin_oauth` and `restore` share one client builder |
62fe32759529 | atagen | 2026-09-26 08:50 UTC | 249 | with an encrypted store, refresh tokens and threads. the store is sqlite |
62fe32759529 | atagen | 2026-09-26 08:50 UTC | 250 | natively and indexeddb in a browser. secret-bearing inputs don't derive `Clone` |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 251 | |
9c05b1dd327a | atagen | 2026-10-08 11:27 UTC | 252 | the homeserver a person typed is checked before the client sees it, and a bad |
9c05b1dd327a | atagen | 2026-10-08 11:27 UTC | 253 | one is `AccountError::InvalidHomeserver`. a bare server name passes untouched, |
9c05b1dd327a | atagen | 2026-10-08 11:27 UTC | 254 | since that's what sends the sdk to `.well-known`, and a host that "helpfully" |
9c05b1dd327a | atagen | 2026-10-08 11:27 UTC | 255 | adds `https://` turns `example.org` into a homeserver url and skips discovery. |
9c05b1dd327a | atagen | 2026-10-08 11:27 UTC | 256 | a url needs a host, no userinfo, and https. plain http needs |
9c05b1dd327a | atagen | 2026-10-08 11:27 UTC | 257 | `allow_insecure_loopback` and a loopback host. `SoftLogout::password_login` sets |
9c05b1dd327a | atagen | 2026-10-08 11:27 UTC | 258 | it, since its homeserver passed once already |
9c05b1dd327a | atagen | 2026-10-08 11:27 UTC | 259 | |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 260 | a `SessionStore` owns one login's record and its store. login saves the first |
f12377bce9b9 | atagen | 2026-09-26 12:32 UTC | 261 | record, `restore` loads it, and the engine a `Session` builds saves every token |
f12377bce9b9 | atagen | 2026-09-26 12:32 UTC | 262 | rotation after that, since a refresh token works once and a host that missed one |
f12377bce9b9 | atagen | 2026-09-26 12:32 UTC | 263 | save has lost the login. the session changes are subscribed the moment the |
f12377bce9b9 | atagen | 2026-09-26 12:32 UTC | 264 | client exists, because the sdk can refresh during restore before any engine |
f12377bce9b9 | atagen | 2026-09-26 12:32 UTC | 265 | does. a save runs off the event stream, so a full stream never holds it back, |
f12377bce9b9 | atagen | 2026-09-26 12:32 UTC | 266 | and `SessionTokensChanged` only goes out once it has landed, or |
f12377bce9b9 | atagen | 2026-09-26 12:32 UTC | 267 | `SessionSaveFailed` if it didn't. natively the sdk's save callback runs inside |
f12377bce9b9 | atagen | 2026-09-26 12:32 UTC | 268 | the refresh, before the new token is used, and `save_blocking` writes there and |
f12377bce9b9 | atagen | 2026-09-26 12:32 UTC | 269 | then. in a browser there's only the async save, so a tab that dies between a |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 270 | refresh and its save wakes up holding a spent token. a fork patch letting the |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 271 | sdk await an async save before it uses the new token would close that, but it |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 272 | changes how a refresh behaves rather than what's visible, and on 2026-10-22 we |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 273 | held off so the fork doesn't drift that far that fast. upstream taking an async |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 274 | save callback, or the fork already carrying behaviour changes, reopens it. |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 275 | `without_persistence` |
f12377bce9b9 | atagen | 2026-09-26 12:32 UTC | 276 | hands the job back to the host. `EncryptedSessionStore` seals the record with |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 277 | the sdk's `StoreCipher` under the store's passphrase and replaces it by rename, |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 278 | 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 | 279 | state store because sqlite writes custom values there in the clear, and a |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 280 | client can't open its store before it knows the homeserver the record holds. |
713205a95af2 | atagen | 2026-09-26 09:24 UTC | 281 | `discard` deletes the record, then the store, since a record without its store |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 282 | would restore into empty crypto. the host follows `Termination::retain`. |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 283 | `Nothing` discards. `Store` calls `soft_log_out`, which keeps the store and |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 284 | swaps the record for one holding the homeserver, user, device and oauth client |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 285 | id with no tokens. `restore` hands that back as `Restore::SoftLoggedOut`, so a |
6f1d6591c10d | atagen | 2026-09-26 09:52 UTC | 286 | restart still knows which device to log in again as, and the login overwrites |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 287 | it. one store directory per login. a login whose first save fails comes back as |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 288 | `AccountError::Unsaved` holding the live session, so the host retries the save |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 289 | rather than spending another device |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 290 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 291 | `Accounts` picks the store, so a host never decides which account a login form |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 292 | means. it's one owner's accounts under one root and one passphrase, a store and |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 293 | record each, named by a random id. a password login that names a |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 294 | soft-logged-out account, by localpart or full user id on the same server, logs |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 295 | back in as that device into that store. the server is compared by name as well |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 296 | as by url, since a typed `example.org` is stored as wherever discovery led. a |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 297 | live account it names is `AlreadyLoggedIn`, anyone else gets a new store. oauth |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 298 | can't know who it's for until it finishes, so it resumes only the device |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 299 | `SoftLogout::oauth_login` names. a new store is locked before it's listed, so |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 300 | `list` can sweep stores with no record, left by a login that died or a discard |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 301 | that didn't finish, without catching one mid login. a browser lists accounts in |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 302 | an IndexedDB database of their own, a key each, so two tabs adding at once |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 303 | don't lose one |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 304 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 305 | the set is per owner because resuming needs only the password, and the store |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 306 | holds the device's e2ee keys. a set shared across people would hand someone who |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 307 | knows a password the history a fresh login wouldn't get |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 308 | |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 309 | one engine per store. before the client opens the store we take the platform's |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 310 | own exclusive lock on it, `File::try_lock` on `macltok.lock` in the store |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 311 | directory natively and a Web Lock named after the store in a browser. the |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 312 | engine and every `ClientHandle` share it, so it goes when the last of them |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 313 | does, since a lingering handle still has the client and its store open. anyone |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 314 | else, in this process, another one or another tab, gets `StoreInUse` at once, |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 315 | so the host decides whether to wait, hand over or say so. the os or the browser |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 316 | lets go when the holder dies. a SharedWorker serving every tab from one engine |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 317 | never contends for it. Web Locks only exist in a secure context, so a browser |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 318 | store needs one too |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 319 | |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 320 | the sdk's cross process lease can't do this job. in matrix-sdk-common 0.18 |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 321 | `CrossProcessLock::try_lock_once` (`cross_process_lock.rs` 389-448) leases for |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 322 | 500ms and renews every 50ms from a spawned task, which gives up for good on one |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 323 | failed renewal or on finding the lease lapsed and retaken, and tells no one. a |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 324 | stall past half a second, ordinary in a tab and certain during the record's |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 325 | PBKDF2, or one `SQLITE_BUSY`, hands the store to a second engine while the |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 326 | first still thinks it's alone. with our lock keeping everyone else out, clients |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 327 | run with `CrossProcessLockConfig::SingleProcess` and the sdk's own sections skip |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 328 | the lease writes and renewal tasks |
a250c7fa43fb | atagen | 2026-09-26 13:35 UTC | 329 | |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 330 | a pending OAuth login survives the page going away for its redirect. |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 331 | `begin_oauth` saves it as the store's record, sealed like any other since it |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 332 | holds the PKCE verifier, through the fork's `OAuth::pending_authorization`. |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 333 | `Accounts::finish_oauth` takes the callback url and finds the account by its |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 334 | `state`, so the host keeps nothing across the redirect, and |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 335 | `Session::finish_oauth` does the same for one store. a host that keeps the |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 336 | process alive can still use `PendingOAuthLogin::finish`. while it waits, |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 337 | `restore` answers `Restore::OAuthPending`. the record lives in the store |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 338 | rather than with the host because `list` sweeps a store with no record, which |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 339 | would delete a fresh login's store mid redirect |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 340 | |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 341 | the pending record keeps whatever the store held before, so a login resuming a |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 342 | soft-logged-out device that never comes back puts that device back, and a |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 343 | password login can still resume it meanwhile. abort, a failed finish after the |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 344 | redirect and an hour's wait all undo the login, a new account's store deleted |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 345 | and an old record put back. a failed `PendingOAuthLogin::finish` still holds |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 346 | the client, so it puts back an old record and leaves a new one to the hour. |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 347 | codes live minutes, so an hour is generous |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 348 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 349 | ## room lists, spaces and profiles |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 350 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 351 | `RoomSummary` has what a room list row needs without a second query. topic, |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 352 | alias, member counts, heroes for nameless rooms, the inviter, where an upgraded |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 353 | room went, a latest event preview, the joined spaces listing it, and for a |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 354 | space the children it lists |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 355 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 356 | parents come from one lazily built `SpaceService` shared by every handle. |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 357 | children come straight from the space's local state, joined or not. |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 358 | `space_hierarchy` pages through the server's view for browsing rooms you |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 359 | haven't joined, and holds its place between pages |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 360 | |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 361 | `own_profile` and `user_profile` read profiles. name and avatar changes are |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 362 | commands, and uploading an avatar gets the same byte cap as any upload |
960ac0721079 | atagen | 2026-09-25 00:24 UTC | 363 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 364 | ## notifications and room data |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 365 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 366 | the notification client is built lazily and shares the engine's sync service. |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 367 | sdk ui types stay behind the boundary |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 368 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 369 | `rooms` normalises on demand with bounded concurrency and sorts by latest event, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 370 | then name, then id. `RoomsChanged` is an invalidation, coalesced so one sync |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 371 | touching fifty rooms is one event. a sync's room info lands before the event |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 372 | cache has stored its events, so the watcher waits a tenth of a second after |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 373 | anything wakes it and takes the lot as one. an event cache slower than that |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 374 | costs a second one. a left room stays in the list with its |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 375 | membership until it's forgotten. push rule changes never touch room info, so |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 376 | `NotificationSettingsChanged` brings a `RoomsChanged` along for the modes in |
0bfa3f46f0be | atagen | 2026-09-26 09:12 UTC | 377 | the summaries |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 378 | |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 379 | with threads on, the sdk leaves thread events out of a room's counts and keeps |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 380 | none of its own for them. `thread_unreads` counts them per root from the event |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 381 | cache store, which keeps every reply it ever took whatever it holds in memory, |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 382 | so closing a room, the cache trimming itself or a restart leaves them be. a |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 383 | reply counts once it's newer than a receipt for its thread (msc3771), an |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 384 | unthreaded one, or the user's own reply. a main-timeline receipt doesn't reach |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 385 | into threads. the store has lost stream order between a thread and the rest of |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 386 | the room, so an event's timestamp stands for its place, and a receipt on an |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 387 | event the store never had falls back to the receipt's own time. a thread's |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 388 | mentions also count in the room's `unread_mentions`, so nobody misses one, |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 389 | while its messages and notifications stay apart, since they don't move the main |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 390 | timeline's read marker |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 391 | |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 392 | the counts are kept in the state store per room, only for threads with |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 393 | something unread, so `rooms` reads them without touching the event cache and a |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 394 | cold room never gets one made for it. thread events and threaded receipts leave |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 395 | room info alone, so the room list watcher recounts on its own. a synced thread |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 396 | event names the thread to count, an event cache update recounts the room's |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 397 | threads already holding something unread, since a late decryption can turn up |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 398 | a mention, and so does a receipt of the user's own. counting in the sdk would |
21b9227729a4 | atagen | 2026-10-25 11:03 UTC | 399 | be cheaper, but that's a behaviour change in the fork |
e5c6427adb35 | atagen | 2026-10-25 10:23 UTC | 400 | |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 401 | ## link previews |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 402 | |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 403 | a text message can carry its sender's own previews (msc4095), and those come |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 404 | out on `Message::link_previews` without asking anyone, so they work in |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 405 | encrypted rooms and leak nothing. an encrypted preview image is registered like |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 406 | an attachment. a preview whose url isn't in the body is dropped, or a sender |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 407 | could caption any link with any card |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 408 | |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 409 | `url_preview` asks the homeserver, which fetches the page, re-hosts `og:image` |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 410 | and answers with loose `og:` keys. that tells the homeserver every url, so |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 411 | whether encrypted rooms ask is the host's call. answers are kept an hour and |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 412 | misses ten minutes, and asks for one url at once share a request, since synapse |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 413 | rate limits the endpoint and a timeline repeats links. synapse answers a page |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 414 | it couldn't download with a 500, a dns failure with a 502 and its blocklist with |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 415 | a 403, so those, 400 and 404 mean no preview. a genuine 500 hides in there too, |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 416 | but a missing card is cheap. a |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 417 | server that doesn't preview at all says so with `M_UNRECOGNIZED` once and isn't |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 418 | asked again. a 429 anywhere is `ErrorKind::RateLimited`. page text from either |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 419 | source is trimmed, 256 characters for a title or site name and 1024 for a |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 420 | description, and an image that isn't an mxc uri is dropped |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 421 | |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 422 | `url_previews_allowed` follows element, so both agree. the user's own choice |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 423 | for the room, then their account wide default, then room state an admin set. |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 424 | the room state comes in with the rest through the fork's extra required state |
0291516d40b5 | atagen | 2026-10-22 08:31 UTC | 425 | |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 426 | ## image packs |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 427 | |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 428 | msc2545 packs come from the user's account data, from room state, and from the |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 429 | rooms the user enabled everywhere. the room list asks the server for every |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 430 | room's `im.ponies.room_emotes`, through the fork's extra required state, so |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 431 | packs sit in the store like any other state and a change reaches us even across |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 432 | a gap in the timeline. that's every joined room's packs on the first sync, |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 433 | tens of kilobytes for a big one, and only changes after. asking only for open |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 434 | rooms would lose the enabled packs of rooms nobody has open |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 435 | |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 436 | `image_packs` reads all of it from the store and hands back the lot, global |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 437 | packs and each room's own, and `ImagePacks::for_room` puts a picker's list in |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 438 | order. an enabled room the user hasn't joined is skipped, there's no state for |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 439 | it. one bad image costs that image and a pack with nothing usable is dropped, |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 440 | since pack contents are whatever someone's client wrote. `ImagePacksChanged` |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 441 | says a pack moved somewhere, from a state event, an account data change or a |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 442 | membership change taking a room's packs with it. it's an invalidation, and the |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 443 | mirror refetches on it like it does rooms, keeping the packs in its state |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 444 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 445 | ## verification |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 446 | |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 447 | `Accept`, `Confirm`, `Reject` and `Cancel`. a request another device sends |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 448 | shows up as `Incoming` and waits for `Accept`, or `Cancel` to turn it down. one |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 449 | is only taken up while nothing else is in flight. reject tells the other side |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 450 | the emoji didn't match. the watchers subscribe before they look at the current |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 451 | state, since the sdk only reports what changes after you ask. the watcher and |
5a52012c9b1d | atagen | 2026-09-24 23:45 UTC | 452 | the action paths claim the terminal event, so a racing |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 453 | cancel can't turn `Done` into `Cancelled` |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 454 | |
a319bae745d8 | atagen | 2026-09-26 09:59 UTC | 455 | ## mirror |
a319bae745d8 | atagen | 2026-09-26 09:59 UTC | 456 | |
a319bae745d8 | atagen | 2026-09-26 09:59 UTC | 457 | the mirror is a consumer like any other, behind the `mirror` feature, and it |
a319bae745d8 | atagen | 2026-09-26 09:59 UTC | 458 | takes any stream of events rather than the `EventStream` itself so a host keeps |
a319bae745d8 | atagen | 2026-09-26 09:59 UTC | 459 | the raw events without a second channel. state, its sequence number and the |
a319bae745d8 | atagen | 2026-09-26 09:59 UTC | 460 | broadcast send all move under one lock, which is how `subscribe` gets a |
a319bae745d8 | atagen | 2026-09-26 09:59 UTC | 461 | snapshot and a receiver with nothing between them. a lagged subscriber starts |
a319bae745d8 | atagen | 2026-09-26 09:59 UTC | 462 | over from a fresh state under the same lock |
a319bae745d8 | atagen | 2026-09-26 09:59 UTC | 463 | |
a319bae745d8 | atagen | 2026-09-26 09:59 UTC | 464 | nothing on the draining path waits for an engine answer. a resync runs on its |
a319bae745d8 | atagen | 2026-09-26 09:59 UTC | 465 | own task, retrying with backoff until a reset or a close for its timeline |
a319bae745d8 | atagen | 2026-09-26 09:59 UTC | 466 | aborts it. rooms are fetched on a task too, one at a time with one more owed at |
0b8ce8dc9a1c | atagen | 2026-09-26 12:30 UTC | 467 | most. a diff for a timeline with no reset is a divergence too, so a wrapper |
0b8ce8dc9a1c | atagen | 2026-09-26 12:30 UTC | 468 | that swallowed the reset still heals. anything a late subscriber needs, failures |
0b8ce8dc9a1c | atagen | 2026-09-26 12:30 UTC | 469 | included, lives in the state rather than only in an update |
a319bae745d8 | atagen | 2026-09-26 09:59 UTC | 470 | |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 471 | ## search |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 472 | |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 473 | `search_messages` takes words and a scope of one room, a list of them or every |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 474 | joined room, and answers newest first. it opens a search the way a jump opens an |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 475 | event context, a session owning the media of every hit it hands out under its |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 476 | own bounded scope, paged with `search_more` and its cursor and freed with |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 477 | `close_search`. at most eight stay open and opening another closes the oldest, |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 478 | so a host that never closes anything still holds a bounded amount, and the |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 479 | closed one goes out as `SearchClosed`. a hit's media ids carry their search's |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 480 | owner prefix like a context's, and stop resolving once it closes. encrypted |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 481 | rooms can't use the server's `/search`, so everything goes through one local |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 482 | index, unencrypted |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 483 | rooms too, so results and coverage mean the same thing everywhere. a hit comes |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 484 | back the way event context shows its target, latest edit applied, with byte |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 485 | ranges of the matched words in its text |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 486 | |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 487 | the index is tantivy, but not the fork's matrix-sdk-search. that crate keeps its |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 488 | schema private, keeps an index per room, opens a fresh `IndexWriter` for every |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 489 | event, ranks by score, hands back bare event ids and turns mmap on, which wasm |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 490 | can't build. `IndexWriter` can't run in wasm either, since it spawns indexing |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 491 | threads and rayon pools. so macltok keeps its own segments. each flush writes an |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 492 | immutable segment with `SingleSegmentIndexWriter` into a `RamDirectory`, edits |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 493 | and redactions are tombstones in a bitset beside it, and segments of a size |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 494 | class merge through `merge_filtered_segments` once six pile up, dropping the |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 495 | tombstoned docs as they go. all of that is single threaded with the docstore's |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 496 | dedicated compressor off. message text is indexed and not stored, a preview |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 497 | reads the event back. the one exception is an edit waiting for its original, |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 498 | whose text is kept so the original can take it over. a 2026-10-25 spike ran the |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 499 | whole cycle in a headless firefox worker |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 500 | |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 501 | it lives with the store, so it's per login and never synced, and it's only |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 502 | opened for the user and homeserver that made it. natively it's |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 503 | `macltok-search.sqlite3` beside the sdk's databases, created 0600, and in a |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 504 | browser the `::macltok-search` IndexedDB database, and both go when the store is |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 505 | deleted. every segment, every tombstone generation, the manifest and the waiting |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 506 | undecryptable events are blobs sealed by a `StoreCipher` under the store |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 507 | passphrase, stored under a keyed hash of their name like the mentions rows, the |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 508 | name sealed inside so blobs can't be swapped. the manifest a commit saves is the |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 509 | index as it stands once that commit lands, and blobs it doesn't point at are |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 510 | dropped on the next load. one commit is one transaction, sqlite in wal with |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 511 | normal sync and IndexedDB relaxed, so a crash can lose the last few seconds of |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 512 | commits but never leaves half of one. the working copy lives in memory, all of |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 513 | it, which is the price of tantivy's directory being synchronous and IndexedDB |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 514 | not. a store that won't open or read, or five commits failing in a row, means |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 515 | an index in memory for the rest of the engine, and coverage says |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 516 | `persistent: false` with the reason in `in_memory` |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 517 | |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 518 | it's fed from what the client sees. each joined room's event cache is followed |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 519 | from its first update, so sync, pagination and the redecryptor's replacements |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 520 | all arrive as diffs, and a room idle for five minutes is let go so the cache can |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 521 | shrink. the generic update channel is followed from before the index loads, and |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 522 | if it lags every joined room is followed, since a lost update could be a room's |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 523 | only one. an edit lands on its original under the original's key, the latest by |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 524 | the original's sender winning. one that arrives first waits under its target and |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 525 | its own sender, out of results, until the original turns up and takes the |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 526 | author's edit, so a stranger's early edit can't crowd it out. an edit of an edit |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 527 | is ignored, and an edit to content we can't read clears the old text. a |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 528 | redaction drops the key and is remembered for a while, so a stale copy can't |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 529 | bring it back. redacting the latest edit reads the original again with whatever |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 530 | edits of it are still around. a message still encrypted waits, raw, up to two |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 531 | thousand of them in sixteen blobs, and is decrypted eight at a time when a room |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 532 | key for its session arrives |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 533 | |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 534 | backfill is `/messages` straight from the room, not through the event cache, |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 535 | so it never grows a timeline someone has open, filtered to messages, encrypted |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 536 | events, redactions, stickers and polls. one request at a time across every |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 537 | room, two seconds apart, a hundred events a page and ten pages a room per |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 538 | engine run, round robin. a room walks back from the newest event to the start, |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 539 | and resumes from its saved token next time. a room that's been indexed before |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 540 | also gets a head pass the first time it shows up in a run, from the newest event |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 541 | back until it meets something that was already indexed when the engine started, |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 542 | which picks up what happened while we were offline. a failing room backs off on |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 543 | its own from a minute to fifteen. a saved token the server turns down with a 400 |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 544 | is dropped and the room starts again from the newest. leaving a room cancels its |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 545 | page. all of that is `EngineConfig`, and zero pages a room turns backfill off |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 546 | |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 547 | coverage is per room, the oldest and newest indexed message, whether backfill |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 548 | reached the start, and where backfill is at, plus the undecryptable messages in |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 549 | scope. it comes with every page |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 550 | |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 551 | the analyser makes a token of every run of letters and digits up to 64 chars, |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 552 | so hashes and long words fit, and lowercases and folds accents, so `cafe` finds |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 553 | `Café`. han, kana, hangul and thai have no spaces to split on, so every char is |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 554 | a token and so is every pair of neighbours, and a query for a run needs all its |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 555 | pairs. that finds a word inside a sentence at the cost of the odd match across a |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 556 | word boundary. emoji are tokens too. queries have no field syntax, so `re:` or a |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 557 | url is just its words. words all have to match, quotes make a phrase and `-` |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 558 | excludes, and a query with nothing left to find is `Invalid` rather than |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 559 | everything in scope. there's no stemming |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 560 | |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 561 | a hit is read back when it's shown, `load_or_fetch_event` for the original and |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 562 | for its latest edit, which is the event cache and its store first and the |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 563 | server's `/event` after, eight at a time. the original's bundled edit is ignored, |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 564 | since the index already knows the newest one. one that's turned out redacted is |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 565 | dropped from the page and from the index. one that can't be read at all, gone |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 566 | from the cache with the server unreachable or refusing, is left out of that page |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 567 | but stays indexed, so it shows up again once it can be read. the cursor still |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 568 | moves past it |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 569 | |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 570 | tantivy's merger times itself with `Instant::now()` whenever a `log` logger is on |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 571 | at debug, and that panics in wasm. on 2026-10-25 we capped `log` at info at |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 572 | compile time in wasm, `max_level_info` and `release_max_level_info` both, since |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 573 | in release the release cap wins. it's all or nothing for the whole wasm build, so |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 574 | a host gives up `log` debug and trace from every crate, not just tantivy, and |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 575 | asking for any other cap is log's own "multiple max_level_* features set" error. |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 576 | tantivy timing with something wasm can call, or reading `Instant` only behind |
e521a7177da3 | atagen | 2026-10-25 11:18 UTC | 577 | its own feature, reopens it |
cff1b0a4eb30 | atagen | 2026-10-25 10:42 UTC | 578 | |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 579 | ## the sdk fork |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 580 | |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 581 | we build against a fork of matrix-rust-sdk, branch `macltok`, cut from the |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 582 | matrix-sdk 0.18.0 tag. turned down on 2026-09-26, taken up on 2026-10-22 once |
7515819c2f3e | atagen | 2026-10-22 13:03 UTC | 583 | it was holding back features every peer client has. the sdk crates are git |
7515819c2f3e | atagen | 2026-10-22 13:03 UTC | 584 | dependencies pinned to a fork rev, so dependents get the fork through macltok |
7515819c2f3e | atagen | 2026-10-22 13:03 UTC | 585 | with no patch block of their own, and macltok stays off crates.io until |
7515819c2f3e | atagen | 2026-10-22 13:03 UTC | 586 | upstream takes the patches. a dependent naming matrix-sdk itself has to point |
7515819c2f3e | atagen | 2026-10-22 13:03 UTC | 587 | at the same rev or it gets a second sdk |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 588 | |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 589 | it carries visibility and config changes, each small enough to be an upstream |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 590 | pr, and nothing that changes how the sdk behaves. if that stops being true, |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 591 | especially in the timeline or the send queue, it's time to drop matrix-sdk and |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 592 | matrix-sdk-ui for our own layer over matrix-sdk-base and matrix-sdk-crypto, |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 593 | which keep their public sans-io apis |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 594 | |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 595 | - `Room::prepare_encryption`, the send path's encryption prep made public |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 596 | - `SyncServiceBuilder::with_room_list_extra_required_state`, for state the |
17e042fbc331 | atagen | 2026-10-08 12:39 UTC | 597 | sdk's hard-coded list leaves out. image packs use it |
8fa8a7cde89c | atagen | 2026-10-08 12:39 UTC | 598 | - msc4027 reaction shortcodes, kept on the timeline's `ReactionInfo` and sent |
8fa8a7cde89c | atagen | 2026-10-08 12:39 UTC | 599 | with `Timeline::toggle_reaction_with_shortcode` |
411bc1c78b65 | atagen | 2026-10-22 07:39 UTC | 600 | - `SendQueue::enable_global_upload_progress`, upload progress on the |
411bc1c78b65 | atagen | 2026-10-22 07:39 UTC | 601 | client-wide send queue channel |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 602 | - `OAuth::pending_authorization` and `restore_pending_authorization`, to |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 603 | finish an oauth login in another process |
dc609022bb53 | atagen | 2026-10-08 12:39 UTC | 604 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 605 | ## public model |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 606 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 607 | owned ruma ids and typed data wherever there's a stable shape, `Raw` where |
e9b6478f917f | atagen | 2026-10-08 11:14 UTC | 608 | arbitrary content has to survive. ruma is re-exported as `macltok::ruma`, so a |
e9b6478f917f | atagen | 2026-10-08 11:14 UTC | 609 | host parses ids at the version we're typed against. anything expected to grow is |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 610 | `#[non_exhaustive]`. `Engine::from_client` stays public for people who built |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 611 | their own client |