atagen

macltok

public
Star0
Watch0
fa9114fcf75f7bdf1a7cec3b3367d604cf7de472
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
docs/design-decisions.mdMarkdown172 lines9.1 KB

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.

  • Opening owns the generation and actor request sender
  • Open owns the generation and actor request sender
  • Closing owns 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. logout and a rejected token both stop the engine. logout and a hard rejection kill the record and the store, a soft logout keeps the store for a same-device login, Stopped alone kills nothing. 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.