Files
simplexmq/spec/modules/NOTES.md
Evgeny @ SimpleX Chat fc5b601cb4 notes
2026-03-13 21:45:24 +00:00

11 KiB

Design Notes

Non-bug observations from module specs that are worth tracking. These remain documented in their respective module specs — this file serves as an index.

Backend Observations

N-01: SNotifier path doesn't cache

Location: Simplex.Messaging.Server.QueueStore.PostgresgetQueues_ SNotifier branch Description: The SRecipient path caches loaded queues via cacheRcvQueue with double-check locking. The SNotifier path does NOT cache — it uses a stale TMap snapshot and maybe (mkQ False rId qRec) pure, so concurrent loads for the same notifier can create duplicate ephemeral queue objects. Functionally correct but wasteful. Module spec: QueueStore/Postgres.md

N-02: assertUpdated error conflation

Location: Simplex.Messaging.Server.QueueStore.PostgresassertUpdated Description: assertUpdated returns AUTH for zero-rows-affected. This is the same error code used for "not found" (via readQueueRecIO) and "duplicate" (via handleDuplicate). The actual cause — stale cache, deleted queue, or constraint violation — is indistinguishable in logs. Module spec: QueueStore/Postgres.md

Design Characteristics

N-03: RCVerifiedInvitation constructor exported

Location: Simplex.RemoteControl.InvitationRCVerifiedInvitation Description: RCVerifiedInvitation is a newtype with constructor exported via (..). It can be constructed without calling verifySignedInvitation, bypassing signature verification. The trust boundary is conventional, not enforced by the type system. connectRCCtrl accepts only RCVerifiedInvitation. Module spec: RemoteControl/Invitation.md

N-04: smpEncode Word16 silent truncation

Location: Simplex.Messaging.EncodingEncoding Word16 instance Description: smpEncode for ByteString uses a 1-byte length prefix. Maximum encodable length is 255 bytes. Longer values silently wrap via w2c . fromIntegral. Callers must ensure ByteStrings fit or use Large. Module spec: Encoding.md

N-05: writeIORef for period stats — not atomic

Location: Simplex.Messaging.Server.StatssetPeriodStats Description: Uses writeIORef (not atomic). Only safe during router startup when no other threads are running. If called concurrently, period data could be corrupted. Module spec: Server/Stats.md

N-06: setStatsByServer orphans old TVars

Location: Simplex.Messaging.Notifications.Server.StatssetStatsByServer Description: Builds a fresh Map Text (TVar Int) in IO, then atomically replaces the TMap's root TVar. Old per-router TVars are not reused — any other thread holding a reference from a prior TM.lookupIO would modify an orphaned counter. Called at startup, but lacks the explicit "not thread safe" comment. Module spec: Notifications/Server/Stats.md

N-07: Lazy.unPad doesn't validate data length

Location: Simplex.Messaging.Crypto.LazyunPad / splitLen Description: splitLen does not validate that the remaining data is at least len bytes — LB.take len silently returns a shorter result. The source comment notes this is intentional to avoid consuming all lazy chunks for validation. Module spec: Crypto/Lazy.md

N-08: Batched commands have no timeout-based expiry

Location: Simplex.Messaging.ClientsendBatch Description: Batched commands are written with Nothing as the request parameter — the send thread skips the pending flag check. Individual commands have timeout-based expiry. If the router stops returning results, batched commands can block the send queue indefinitely. Module spec: Client.md

N-09: Postgres MsgStore nanosecond precision

Location: Simplex.Messaging.Server.MsgStore.PostgrestoMessage Description: MkSystemTime ts 0 constructs timestamps with zero nanoseconds. Only whole seconds are stored. Messages read from Postgres have coarser timestamps than STM/Journal stores. Not a practical issue — timestamps are typically rounded to hours or days. Module spec: Server/MsgStore/Postgres.md

N-10: MsgStore Postgres — error stubs crash at runtime

Location: Simplex.Messaging.Server.MsgStore.Postgres — multiple MsgStoreClass methods Description: Multiple MsgStoreClass methods are error "X not used". Required by the type class but not applicable to Postgres. Calling any at runtime crashes. Safe because Postgres overrides the relevant default methods, but a new caller using the wrong method would crash with no compile-time warning. Module spec: Server/MsgStore/Postgres.md

N-11: strP default assumes base64url for all types

Location: Simplex.Messaging.Encoding.StringStrEncoding class default Description: The MINIMAL pragma allows defining only strDecode without strP. The default strP = strDecode <$?> base64urlP assumes input is base64url-encoded for any type. A new StrEncoding instance that defines only strDecode for non-base64 data would get a broken parser. Module spec: Encoding/String.md

Silent Behaviors

Intentional design choices that are correct but non-obvious. A code modifier who doesn't know these could introduce bugs.

N-12: Service signing silently skipped on empty authenticator

Location: Simplex.Messaging.Client — service signature path Description: The service signature is only added when the entity authenticator is non-empty. If authenticator generation fails silently (returns empty bytes), service signing is silently skipped. Module spec: Client.md

N-13: stmDeleteNtfToken — nonexistent token indistinguishable from empty

Location: Simplex.Messaging.Notifications.Server.StorestmDeleteNtfToken Description: If the token ID doesn't exist in the tokens map, the registration-cleanup branch is skipped and the function returns an empty list. The caller cannot distinguish "deleted a token with no subscriptions" from "token never existed." Module spec: Notifications/Server/Store.md

N-14: createCommand silently drops commands for deleted connections

Location: Simplex.Messaging.Agent.Store.AgentStorecreateCommand Description: When createCommand encounters a constraint violation (the referenced connection was already deleted), it logs the error and returns successfully. Commands targeting deleted connections are silently dropped. Module spec: Agent/Store/AgentStore.md

N-15: Redirect chain loading errors silently swallowed

Location: Simplex.Messaging.Agent.Store.AgentStore Description: When loading redirect chains, errors loading individual redirect files are silently swallowed via either (const $ pure Nothing) (pure . Just). Prevents a corrupt redirect from blocking access to the main file. Module spec: Agent/Store/AgentStore.md

N-16: BLOCKED encoded as AUTH for old XFTP clients

Location: Simplex.FileTransfer.ProtocolencodeProtocol Description: If the protocol version is below blockedFilesXFTPVersion, a BLOCKED result is encoded as AUTH instead. The blocking information (reason) is permanently lost for these clients. Module spec: FileTransfer/Protocol.md

N-17: restore_messages three-valued logic with implicit default

Location: Simplex.Messaging.Server.Main — INI config Description: The restore_messages INI setting has three-valued logic: explicit "on" → restore, explicit "off" → skip, missing → inherits from enable_store_log. This implicit default is not captured in the type system — callers see Maybe Bool. Module spec: Server/Main.md

N-18: Stats format migration permanently loses precision

Location: Simplex.Messaging.Server.StatsstrP for ServerStatsData Description: The parser handles multiple format generations. Old format qDeleted= is read as (value, 0, 0). qSubNoMsg is parsed and discarded. subscribedQueues is parsed but replaced with empty data. Data loaded from old formats is coerced — precision is permanently lost. Module spec: Server/Stats.md

N-19: resubscribe exceptions silently lost

Location: Simplex.Messaging.Notifications.Serverresubscribe Description: resubscribe is launched via forkIO before raceAny_ starts — not part of the raceAny_ group. Most exceptions are silently lost per forkIO semantics. ExitCode exceptions are special-cased by GHC's runtime and do propagate. Module spec: Notifications/Server.md

N-20: closeSMPClientAgent worker cancellation is fire-and-forget

Location: Simplex.Messaging.Client.AgentcloseSMPClientAgent Description: Executes in order: set active = False, close all client connections, swap workers map to empty and fork cancellation threads. Cancel threads use uninterruptibleCancel but are fire-and-forget — the function may return before all workers are cancelled. Module spec: Client/Agent.md

N-21: APNS unknown 410 reasons trigger retry instead of permanent failure

Location: Simplex.Messaging.Notifications.Server.Push.APNS Description: Unknown 410 (Gone) reasons fall through to PPRetryLater, while unknown 400 and 403 reasons fall through to PPResponseError. An unexpected APNS 410 reason string triggers retry rather than permanent failure. Module spec: Notifications/Server/Push/APNS.md

N-22: NTInvalid/NTExpired tokens can create subscriptions

Location: Simplex.Messaging.Notifications.Protocol — token status permissions Description: Token status NTInvalid allows subscription commands (SNEW, SCHK, SDEL). A TODO comment explains: invalidation can happen after verification, and existing subscriptions should remain manageable. NTExpired is also permitted. Module spec: Notifications/Protocol.md

N-23: removeInactiveTokenRegistrations doesn't clean up empty inner maps

Location: Simplex.Messaging.Notifications.Server.StorestmRemoveInactiveTokenRegistrations Description: stmDeleteNtfToken checks whether inner TMap is empty after removal and cleans up the outer key. stmRemoveInactiveTokenRegistrations does not — surviving active tokens' registrations remain, but empty inner maps can persist. Module spec: Notifications/Server/Store.md

N-24: cbNonce silently truncates or pads

Location: Simplex.Messaging.CryptocbNonce Description: If the input is longer than 24 bytes, it is silently truncated. If shorter, it is silently padded. No error is raised. Callers must ensure correct length. Module spec: Crypto.md