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.Postgres — getQueues_ 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.Postgres — assertUpdated
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.Invitation — RCVerifiedInvitation
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.Encoding — Encoding 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.Stats — setPeriodStats
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.Stats — setStatsByServer
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.Lazy — unPad / 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.Client — sendBatch
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.Postgres — toMessage
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.String — StrEncoding 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.Store — stmDeleteNtfToken
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.AgentStore — createCommand
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.Protocol — encodeProtocol
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.Stats — strP 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.Server — resubscribe
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.Agent — closeSMPClientAgent
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.Store — stmRemoveInactiveTokenRegistrations
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.Crypto — cbNonce
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