atagen

macltok

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

Blame: README.md

96fdf90aacd6atagen2026-09-08 08:24 UTC1
# macltok
96fdf90aacd6atagen2026-09-08 08:24 UTC2
96fdf90aacd6atagen2026-09-08 08:24 UTC3
`macltok` is a compact Rust application backend built above `matrix-sdk` and
96fdf90aacd6atagen2026-09-08 08:24 UTC4
`matrix-sdk-ui`. It aims to provide the stateful behavior needed by a useful
96fdf90aacd6atagen2026-09-08 08:24 UTC5
Matrix client without depending on a GUI toolkit or inventing another protocol
96fdf90aacd6atagen2026-09-08 08:24 UTC6
SDK.
96fdf90aacd6atagen2026-09-08 08:24 UTC7
96fdf90aacd6atagen2026-09-08 08:24 UTC8
The crate currently provides
96fdf90aacd6atagen2026-09-08 08:24 UTC9
96fdf90aacd6atagen2026-09-08 08:24 UTC10
- typed commands, owned UI-safe events and bounded per-client transport
96fdf90aacd6atagen2026-09-08 08:24 UTC11
- Sliding Sync with normalized connection and room-list state
96fdf90aacd6atagen2026-09-08 08:24 UTC12
- independently retained live room timelines and backwards pagination
96fdf90aacd6atagen2026-09-08 08:24 UTC13
- text, emote, reply, edit, redaction, reaction, receipt and typing operations
96fdf90aacd6atagen2026-09-08 08:24 UTC14
- raw encrypted or unencrypted media acquisition with configurable byte limits
96fdf90aacd6atagen2026-09-08 08:24 UTC15
- encryption state, recovery-key import and SAS verification
62fe32759529atagen2026-09-26 08:50 UTC16
- password login and session restoration with caller-owned encrypted SQLite or
62fe32759529atagen2026-09-26 08:50 UTC17
  IndexedDB stores
96fdf90aacd6atagen2026-09-08 08:24 UTC18
96fdf90aacd6atagen2026-09-08 08:24 UTC19
Matrix room, event, user, device and transaction identifiers remain strongly
96fdf90aacd6atagen2026-09-08 08:24 UTC20
typed. A toolkit timeline identity covers local echoes without pretending they
96fdf90aacd6atagen2026-09-08 08:24 UTC21
already have remote event IDs.
96fdf90aacd6atagen2026-09-08 08:24 UTC22
96fdf90aacd6atagen2026-09-08 08:24 UTC23
## Consuming the engine
96fdf90aacd6atagen2026-09-08 08:24 UTC24
31ce2453c82fatagen2026-09-24 13:07 UTC25
spawn `Engine::run`, drain the `EventStream` from its own task, and talk to the
31ce2453c82fatagen2026-09-24 13:07 UTC26
engine through `ClientHandle`. every command answers once, awaited
31ce2453c82fatagen2026-09-24 13:07 UTC27
(`handle.send_message(..).await?`) or as a `Pending` you can await later or
31ce2453c82fatagen2026-09-24 13:07 UTC28
drop. a sent message answers when the sdk send queue has it, delivery shows up
31ce2453c82fatagen2026-09-24 13:07 UTC29
as the timeline item's send state. reads like `rooms`, `members` and `media`
31ce2453c82fatagen2026-09-24 13:07 UTC30
go straight to the sdk. the stream only carries state
31ce2453c82fatagen2026-09-24 13:07 UTC31
b2b7e7479384atagen2026-09-26 09:40 UTC32
some commands put an event on the stream before they answer. `Open`, `Close`,
b2b7e7479384atagen2026-09-26 09:40 UTC33
`OpenThread`, `CloseThread`, `Resync`, `Paginate`, `Logout` and the
b2b7e7479384atagen2026-09-26 09:40 UTC34
verification actions all do. the stream is bounded, so a task that drains it
b2b7e7479384atagen2026-09-26 09:40 UTC35
and awaits one of those answers deadlocks once it fills. from the draining
b2b7e7479384atagen2026-09-26 09:40 UTC36
task, `try_send` the command and drop the `Pending`, the events tell you how it
b2b7e7479384atagen2026-09-26 09:40 UTC37
went
b2b7e7479384atagen2026-09-26 09:40 UTC38
38c4caecdb3datagen2026-09-26 13:35 UTC39
`prepare_to_send` does the key work a send into an encrypted room would start
38c4caecdb3datagen2026-09-26 13:35 UTC40
with, ahead of time, so the send itself is quicker. the engine runs it itself
38c4caecdb3datagen2026-09-26 13:35 UTC41
when a room opens or `set_typing(room, true)` comes in, unless
38c4caecdb3datagen2026-09-26 13:35 UTC42
`EngineConfig::with_prepare_to_send(false)`. only part of that work is reachable
38c4caecdb3datagen2026-09-26 13:35 UTC43
without a matrix-sdk fork, `docs/design-decisions.md` says which
38c4caecdb3datagen2026-09-26 13:35 UTC44
d5d8097c3afdatagen2026-09-26 09:33 UTC45
start a timeline from `TimelineReset`, then apply each `TimelineChanged` whole
d5d8097c3afdatagen2026-09-26 09:33 UTC46
with `TimelineDiff::try_apply_all`. snapshots are `imbl::Vector`s, re-exported
d5d8097c3afdatagen2026-09-26 09:33 UTC47
as `macltok::imbl`, so a copy costs nothing. when a batch doesn't fit none of it
d5d8097c3afdatagen2026-09-26 09:33 UTC48
lands. stop applying, call `resync`, and pick up from the next `TimelineReset`
96fdf90aacd6atagen2026-09-08 08:24 UTC49
5b21b919d1e6atagen2026-09-27 11:47 UTC50
`media` answers with the whole file, decrypted and checked, which is wrong for a
5b21b919d1e6atagen2026-09-27 11:47 UTC51
long video. `media_source` says where the same bytes live instead, the url and,
5b21b919d1e6atagen2026-09-27 11:47 UTC52
for an encrypted file, its AES-CTR key, iv and hash. `access_token` is what to
5b21b919d1e6atagen2026-09-27 11:47 UTC53
send with it. a host can then fetch and decrypt ranges itself, but the hash
5b21b919d1e6atagen2026-09-27 11:47 UTC54
covers the whole ciphertext, so a range read plays bytes nobody checked
5b21b919d1e6atagen2026-09-27 11:47 UTC55
713205a95af2atagen2026-09-26 09:24 UTC56
with `sqlite` or `indexeddb`, a `SessionStore` holds one login's record and its
713205a95af2atagen2026-09-26 09:24 UTC57
store. `EncryptedSessionStore` seals the record with the store's passphrase, in a
713205a95af2atagen2026-09-26 09:24 UTC58
file you pick natively or an IndexedDB database beside the store in a browser.
f12377bce9b9atagen2026-09-26 12:32 UTC59
logging in saves the first record and `Session::restore` loads it. from then on
f12377bce9b9atagen2026-09-26 12:32 UTC60
the engine the `Session` builds keeps it current. every token rotation is saved
f12377bce9b9atagen2026-09-26 12:32 UTC61
before `SessionTokensChanged` goes out, and a save that fails says so with
f12377bce9b9atagen2026-09-26 12:32 UTC62
`SessionSaveFailed`. natively `EncryptedSessionStore` writes from inside the
f12377bce9b9atagen2026-09-26 12:32 UTC63
sdk's refresh, before the new refresh token is ever used, and a store of your
f12377bce9b9atagen2026-09-26 12:32 UTC64
own gets the same by implementing `SessionStore::save_blocking`. in a browser
f12377bce9b9atagen2026-09-26 12:32 UTC65
the save can only follow the refresh, so a tab killed in between wakes with a
f12377bce9b9atagen2026-09-26 12:32 UTC66
spent token. `Session::without_persistence` hands the job back, and then it's
f12377bce9b9atagen2026-09-26 12:32 UTC67
yours to save `ClientHandle::session_record()` on every `SessionTokensChanged`
fa9114fcf75fatagen2026-09-24 12:51 UTC68
fe4133dbcd47atagen2026-09-26 09:05 UTC69
`Engine::run` resolves to a `Termination` saying why it stopped, and
fe4133dbcd47atagen2026-09-26 09:05 UTC70
`Termination::retain` says what to do about it. after `LoggedOut` or
713205a95af2atagen2026-09-26 09:24 UTC71
`AccessRevoked` the record and the store are dead, `discard` them once the
713205a95af2atagen2026-09-26 09:24 UTC72
client is dropped, since an open store won't go quietly. `SoftLoggedOut` wants
6f1d6591c10datagen2026-09-26 09:52 UTC73
the store kept and a fresh login as the same device, or the e2ee keys go with
6f1d6591c10datagen2026-09-26 09:52 UTC74
it. `soft_log_out` swaps the record for one naming that device, so a restart
6f1d6591c10datagen2026-09-26 09:52 UTC75
gets `Restore::SoftLoggedOut` and `SoftLogout::password_login` rather than
6f1d6591c10datagen2026-09-26 09:52 UTC76
nothing, and the login overwrites it with a live record. `ShutDown` and
6f1d6591c10datagen2026-09-26 09:52 UTC77
`HandlesDropped` keep everything. an `Err` is a real failure and kills nothing.
6f1d6591c10datagen2026-09-26 09:52 UTC78
one store directory per login, the sdk won't share a crypto store between
6f1d6591c10datagen2026-09-26 09:52 UTC79
devices
96fdf90aacd6atagen2026-09-08 08:24 UTC80
cb0668d30c14atagen2026-10-08 11:34 UTC81
`Accounts` does that bookkeeping. it holds one person's accounts under one root,
cb0668d30c14atagen2026-10-08 11:34 UTC82
`login_password` and `begin_oauth` pick the store, resuming a soft-logged-out
cb0668d30c14atagen2026-10-08 11:34 UTC83
device when the login names it, and `list` hands back an `Account` each to
cb0668d30c14atagen2026-10-08 11:34 UTC84
`restore` and to apply `Termination::retain` through `sessions`
cb0668d30c14atagen2026-10-08 11:34 UTC85
a319bae745d8atagen2026-09-26 09:59 UTC86
## The mirror
a319bae745d8atagen2026-09-26 09:59 UTC87
a319bae745d8atagen2026-09-26 09:59 UTC88
the loop above is easy to get subtly wrong, so the `mirror` feature ships one.
a319bae745d8atagen2026-09-26 09:59 UTC89
`Mirror::new` takes a `ClientHandle` and the events, and `Mirror::run` drains
a319bae745d8atagen2026-09-26 09:59 UTC90
them into one `State` with the rooms, every open timeline, connection,
a319bae745d8atagen2026-09-26 09:59 UTC91
encryption, verification, typing, uploads and where the session stands. spawn
a319bae745d8atagen2026-09-26 09:59 UTC92
it the way you spawn `Engine::run`. it takes any stream of events, so wrap the
a319bae745d8atagen2026-09-26 09:59 UTC93
`EventStream` if you want to watch the raw ones go past
a319bae745d8atagen2026-09-26 09:59 UTC94
a319bae745d8atagen2026-09-26 09:59 UTC95
`MirrorHandle` is cheap to clone, one per frontend. `snapshot` hands back the
a319bae745d8atagen2026-09-26 09:59 UTC96
current `State`, and `subscribe` opens with the whole `State` and then every
a319bae745d8atagen2026-09-26 09:59 UTC97
`Update` after it, numbered, with nothing missed and nothing twice. a timeline
a319bae745d8atagen2026-09-26 09:59 UTC98
update carries its diffs and the items they left, so apply the diffs or
a319bae745d8atagen2026-09-26 09:59 UTC99
re-render the list, whichever suits. a subscriber that falls behind gets a fresh
a319bae745d8atagen2026-09-26 09:59 UTC100
`State` instead of an error
a319bae745d8atagen2026-09-26 09:59 UTC101
0b8ce8dc9a1catagen2026-09-26 12:30 UTC102
a batch that doesn't fit leaves the timeline diverged at its last good items,
0b8ce8dc9a1catagen2026-09-26 12:30 UTC103
and so does a diff for a timeline it never saw reset. the mirror asks for the
0b8ce8dc9a1catagen2026-09-26 12:30 UTC104
resync itself, off the draining path, and keeps asking until the reset or a
0b8ce8dc9a1catagen2026-09-26 12:30 UTC105
close turns up. rooms are refetched on `RoomsChanged`, one fetch at a time with
0b8ce8dc9a1catagen2026-09-26 12:30 UTC106
at most one more owed
0b8ce8dc9a1catagen2026-09-26 12:30 UTC107
0b8ce8dc9a1catagen2026-09-26 12:30 UTC108
failures stay in the `State`, so a late subscriber sees them too. what stopped
0b8ce8dc9a1catagen2026-09-26 12:30 UTC109
the engine sits in `engine_failure`, and a failed save of the session record in
0b8ce8dc9a1catagen2026-09-26 12:30 UTC110
`session_save_failure` until the next one works. the mirror never touches the
0b8ce8dc9a1catagen2026-09-26 12:30 UTC111
store
a319bae745d8atagen2026-09-26 09:59 UTC112
62fe32759529atagen2026-09-26 08:50 UTC113
## wasm
62fe32759529atagen2026-09-26 08:50 UTC114
62fe32759529atagen2026-09-26 08:50 UTC115
macltok builds for `wasm32-unknown-unknown`. the `indexeddb` feature gives you
62fe32759529atagen2026-09-26 08:50 UTC116
`StoreConfig::indexeddb` in place of `StoreConfig::sqlite`, and asking for either
62fe32759529atagen2026-09-26 08:50 UTC117
on the wrong target won't compile. `Engine::run` hands back a `LocalBoxFuture`
62fe32759529atagen2026-09-26 08:50 UTC118
there, so spawn it with `wasm_bindgen_futures::spawn_local`
62fe32759529atagen2026-09-26 08:50 UTC119
62fe32759529atagen2026-09-26 08:50 UTC120
getrandom picks its browser backend from a cfg flag, and `.cargo/config.toml`
62fe32759529atagen2026-09-26 08:50 UTC121
only reaches this crate's own builds. every cdylib that pulls macltok in needs
62fe32759529atagen2026-09-26 08:50 UTC122
it too
62fe32759529atagen2026-09-26 08:50 UTC123
62fe32759529atagen2026-09-26 08:50 UTC124
```toml
62fe32759529atagen2026-09-26 08:50 UTC125
[target.wasm32-unknown-unknown]
62fe32759529atagen2026-09-26 08:50 UTC126
rustflags = ["--cfg", "getrandom_backend=\"wasm_js\""]
62fe32759529atagen2026-09-26 08:50 UTC127
```
62fe32759529atagen2026-09-26 08:50 UTC128
62fe32759529atagen2026-09-26 08:50 UTC129
what's different in a browser
62fe32759529atagen2026-09-26 08:50 UTC130
7b376d620a3datagen2026-09-26 12:53 UTC131
- a panic doesn't abort anything. it traps the task mid poll and the rest of
7b376d620a3datagen2026-09-26 12:53 UTC132
  the engine carries on in an undefined state and goes quiet, no `Failed` and no
7b376d620a3datagen2026-09-26 12:53 UTC133
  `Stopped`. register `macltok::on_panic` and have it tear the worker down and
7b376d620a3datagen2026-09-26 12:53 UTC134
  start another. natively a panicking command just costs its caller an
7b376d620a3datagen2026-09-26 12:53 UTC135
  `Interrupted`
62fe32759529atagen2026-09-26 08:50 UTC136
- `MediaUploadProgress` only ever reports completion, the sdk's wasm http path
62fe32759529atagen2026-09-26 08:50 UTC137
  has no upload progress
62fe32759529atagen2026-09-26 08:50 UTC138
- the sdk doesn't retry requests or time them out individually. sync still
62fe32759529atagen2026-09-26 08:50 UTC139
  restarts itself with backoff, that part is ours
a250c7fa43fbatagen2026-09-26 13:35 UTC140
- an OAuth login only finishes in the process that began it, since the sdk
a250c7fa43fbatagen2026-09-26 13:35 UTC141
  keeps its pending state in memory. run it in a popup, or in a worker that
a250c7fa43fbatagen2026-09-26 13:35 UTC142
  outlives the redirect, not by navigating the page away
a250c7fa43fbatagen2026-09-26 13:35 UTC143
- one engine per store. a second tab or worker opening a store that's in use
a250c7fa43fbatagen2026-09-26 13:35 UTC144
  gets `StoreInUse` straight away, so serve many tabs from one engine in a
a250c7fa43fbatagen2026-09-26 13:35 UTC145
  SharedWorker rather than one each. the store's Web Lock needs a secure
a250c7fa43fbatagen2026-09-26 13:35 UTC146
  context
6db49c0ecf71atagen2026-09-26 12:41 UTC147
- `discard` while another tab still has the store open answers
6db49c0ecf71atagen2026-09-26 12:41 UTC148
  `StoreDeletePending`. the delete lands once that tab lets go, and reopening
6db49c0ecf71atagen2026-09-26 12:41 UTC149
  the store waits behind it
62fe32759529atagen2026-09-26 08:50 UTC150
96fdf90aacd6atagen2026-09-08 08:24 UTC151
## Dependency policy
96fdf90aacd6atagen2026-09-08 08:24 UTC152
96fdf90aacd6atagen2026-09-08 08:24 UTC153
UI frameworks, image decoders, application-directory discovery and logging
96fdf90aacd6atagen2026-09-08 08:24 UTC154
subscribers do not belong in this crate. SQLite remains optional through the
62fe32759529atagen2026-09-26 08:50 UTC155
`sqlite` and `bundled-sqlite` features, IndexedDB through `indexeddb`. Media
62fe32759529atagen2026-09-26 08:50 UTC156
decoding belongs to the consumer.
96fdf90aacd6atagen2026-09-08 08:24 UTC157
96fdf90aacd6atagen2026-09-08 08:24 UTC158
## Verification
96fdf90aacd6atagen2026-09-08 08:24 UTC159
96fdf90aacd6atagen2026-09-08 08:24 UTC160
```sh
96fdf90aacd6atagen2026-09-08 08:24 UTC161
nix develop --command cargo fmt -- --check
96fdf90aacd6atagen2026-09-08 08:24 UTC162
nix develop --command cargo test --all-targets
96fdf90aacd6atagen2026-09-08 08:24 UTC163
nix develop --command cargo test --all-targets --features sqlite
96fdf90aacd6atagen2026-09-08 08:24 UTC164
nix develop --command cargo clippy --all-targets -- -D warnings
96fdf90aacd6atagen2026-09-08 08:24 UTC165
nix develop --command cargo clippy --all-targets --features sqlite -- -D warnings
96fdf90aacd6atagen2026-09-08 08:24 UTC166
nix flake check
96fdf90aacd6atagen2026-09-08 08:24 UTC167
```
a33777af1c26atagen2026-09-26 12:54 UTC168
a33777af1c26atagen2026-09-26 12:54 UTC169
`--all-features` doesn't build and isn't meant to. `sqlite` and `indexeddb` each
a33777af1c26atagen2026-09-26 12:54 UTC170
belong to one target and refuse the other