macltok design decisions
This document describes the contract implemented in the working tree.
Engine execution
The engine keeps a contextual task registry. Every spawned task is registered
with either its command id and kind, a watcher name, or a room id and
generation. The run loop continuously joins completed tasks and removes their
context. A command-task panic produces its correlated CommandFailed event.
A watcher or current room-actor panic is terminal to the engine. Shutdown aborts
and joins the remaining registered tasks.
The engine emits Authenticated, then ConnectionChanged(Connecting), before
starting the sync service. ConnectionChanged also represents the later SDK
states and the final Stopped transition. Engine::run returns an
EngineError for terminal failure and emits Failed before Stopped when the
event stream remains available.
The driver command channel remains lossless and clients must drain
EventStream independently of command submission. ClientHandle also exposes
a watch receiver for the current connection state. RoomsChanged is an
invalidation event. Call ClientHandle::rooms for current, normalised room
summaries.
ClientHandle sends Command values and receives a CommandId. Completion is
reported as CommandSucceeded, CommandSucceededWithRoom, or
CommandFailed. CommandFailure contains the id, outer CommandKind, typed
CommandErrorKind, and display message. Direct request and response methods
return CommandResultError, which is either a delivered CommandFailure or a
closed reply channel.
For timeline sends, success means the SDK send queue accepted the event; network delivery is reported later through timeline send state.
send reports enqueueing, not completion. Consumers retain the returned id
until a terminal result with that id arrives; they match CommandFailure::kind
rather than parsing its display message. The event stream must be drained while
commands are submitted, even when several frontend clients share one engine.
Room lifetime and ordering
An open room has an explicit generation and exactly one of three states.
Openingowns the generation and actor request senderOpenowns the generation and actor request senderClosingowns the generation and a bounded FIFO of deferred open and close commands
An Open command starts an actor immediately and returns its terminal result
only after the actor reports readiness. There is no separate readiness waiter or
readiness task. Repeated opens while opening are retained by the actor and each
gets one terminal result. A close changes the room to Closing, rejects work
that needs an open actor, and waits for that generation to exit. Opens and
closes during closing are queued in order; after exit, each deferred command is
replayed, with a fresh generation started when needed. Events from earlier
generations are ignored.
Each room actor serialises its timeline work in FIFO order. Different rooms run concurrently. Room mutations which do not require an open timeline use a separate keyed FIFO scheduler. Jobs with the same room, target, or direct-chat key run in submission order while unrelated keys run concurrently. Notification commands and encryption commands each have one dedicated serial worker. The encryption worker owns the verification controller and cancels it before shutdown completes.
Pagination has a per-timeline mutex gate and a session-owned completion path. Pagination tasks are reaped as they finish. Closing a timeline aborts its pagination work and emits one terminal result for every still-pending command. There is no public pagination handle. The internal result channel is only the session completion mechanism and is not an engine-wide routing design.
TimelineDiff::try_apply rejects indices that do not match the consumer’s
snapshot without modifying that snapshot. A consumer then stops applying diffs
for that timeline and sends TimelineCommand::Resync. The timeline session
aborts and joins its old watcher, atomically subscribes to a fresh SDK snapshot
and diff stream, and emits TimelineReset before the resync command succeeds.
This works for rooms and threads without closing their timelines or pagination.
Timeline diffs and resets carry TimelineId, which distinguishes a room from a
thread by its root event. Thread actors and pagination state belong to the room
actor. Stateful timeline commands require the room actor. Standalone room
commands such as typing, receipts, send-queue control, membership actions, and
moderation use the SDK Room directly.
Account construction and secrets
Session::login_password, Session::begin_oauth, and Session::restore use
one shared account client builder. It configures encrypted SQLite storage,
encryption settings, refresh-token handling, and enabled threading without
thread subscriptions. Session::build passes the authenticated client to the
engine.
Secret-bearing input types do not derive Clone. The store passphrase is
borrowed by the shared builder, password login passes its password by reference,
and authentication records are cloned only where SDK ownership requires it.
The caller owns durable authentication records. Save Session::record() after
login, then consume SessionTokensChanged and atomically replace that saved
record with ClientHandle::session_record(). A previously captured record may
hold an exhausted refresh token; failing to persist a rotation must be surfaced
rather than silently treating the stale record as safe to restore. Restore with
the same StoreConfig path and passphrase. Explicit logout and
SessionAccessRejected invalidate the saved record and store; Stopped alone
does not. Neither cookies nor application-specific secret storage belong in the
toolkit.
Notifications and room data
The notification client is constructed lazily in an OnceCell and shares the
engine sync service. It is created only when notification resolution is first
requested. PushNotification exposes typed room and sender metadata and a
typed PushNotificationEvent. The event is Raw<AnySyncTimelineEvent> for a
timeline notification or Raw<StrippedRoomMemberEvent> for an invite. SDK UI
types do not cross the public boundary.
Room summaries are normalised once per rooms query. ClientHandle::rooms_filtered
normalises its name search term once before filtering. Summary generation runs
display-name requests with bounded concurrency. Sorting is descending latest
event timestamp, then case-insensitive name, then room id as a deterministic tie
break.
Global and room account data retain typed Matrix event-type parameters and raw event content. The handle offers direct raw reads and engine-backed raw writes.
Commands and errors
Command is grouped into Account, Room, Timeline, Media,
Encryption, Notification, and Lifecycle domains. CommandKind identifies
the outer domain in result events.
RoomCommand::InviteUser contains only room and user ids. It deliberately does
not expose a reason because the SDK invite helper accepts none. Kick, ban, and
unban retain optional reasons where the protocol supports them.
TimelineCommand::Edit accepts MessageEditContent. Its Text and Emote
variants carry RichText, so edited text and emotes preserve plain text,
optional Matrix HTML, and mentions. Empty edited bodies fail with EmptyBody.
VerificationAction contains Confirm, Reject, and Cancel. Starting a
verification is a target-bearing encryption command rather than an action.
Verification progress uses Waiting, Comparison, Confirmed, Done,
Cancelled, and typed Failed(VerificationFailure). Request acceptance, SAS
startup and acceptance failures retain their message, while stream termination
has dedicated variants. A cancelled verification can be started again.
The watcher and action paths claim terminal emission so a racing cancellation
cannot replace Done with a local Cancelled event.
CommandErrorKind is the stable typed failure category. It includes room and
timeline availability, queue pressure, validation, media, verification,
authentication, network, server, store, timeline, encryption, and fallback
categories. The rendered message preserves operation context while internal
errors retain their source chains.
Power levels
Power-level reads return the complete local RoomPowerLevels mapping.
PowerLevelChanges applies selected common and well-known event thresholds,
and SetUserPowerLevel changes one user mapping.
The SDK trait that applies a power-level event is crate-private. macltok keeps an atomic local mapping, applies all requested changes to it, and sends one state event. It does not patch upstream and does not send separate events for individual fields.
Public model boundaries
The public model uses owned ruma identifiers and typed data wherever it has a
stable representation. Raw Matrix event payloads remain Raw where arbitrary
event content must be preserved. Public output models are non-exhaustive where
they are expected to grow. SDK clients remain available through the handle for
direct state queries that do not need engine coordination.