on master help sign in
Extract helpers and refine type safety in server handlers
commit6f5e82062911e26dba8b7452c1c8896933fc0fba authoriko <mail@iko.soy> dateThu 9 Apr 2026, 22:37 (6 months ago) parent82fcdc7
11 files changed+73−79
client/App.fs+48−49 client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.pdb client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.AssemblyInfo.fs+1−1 client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.AssemblyInfoInputs.cache+1−1 client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.pdb client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.sourcelink.json+1−1 client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/ref/Skrepka.dll client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/refint/Skrepka.dll server.knot+22−27
client/App.fs+48−49
@@ -20,6 +20,12 @@ module App =
2020 Timestamp: DateTimeOffset
2121 IsOutgoing: bool }
2222
23+ type IncomingMessage =
24+ { FromPubkey: string
25+ Body: string
26+ Timestamp: int64
27+ MessageId: string }
28+
2329 type Page =
2430 | Setup
2531 | Conversations
@@ -68,7 +74,7 @@ module App =
6874 | SetNewNickname of string
6975 | DoSaveContact
7076 | DoDeleteContact of string
71− | PollResult of (string * string * int64 * string) list * string * uint64
77+ | PollResult of IncomingMessage list * string * uint64
7278 | DoPoll
7379 | CopyPubKey
7480 | DismissError
@@ -78,14 +84,7 @@ module App =
7884 type CmdMsg =
7985 | CmdGenIdentity
8086 | CmdConnect of url: string * pubHex: string * privKey: byte[]
81− | CmdSend of
82− url: string *
83− token: string *
84− toHex: string *
85− privKey: byte[] *
86− pubKeyHex: string *
87− recipPubHex: string *
88− text: string
87+ | CmdSend of url: string * token: string * recipientHex: string * privKey: byte[] * text: string
8988 | CmdPoll of url: string * token: string * privKey: byte[] * cursor: uint64
9089 | CmdCopyToClipboard of string
9190
@@ -100,6 +99,19 @@ module App =
10099 |> Option.map _.Nickname
101100 |> Option.defaultValue (truncKey pk)
102101
102+ let private messagesFor (pk: string) (messages: Map<string, ChatMessage list>) =
103+ messages |> Map.tryFind pk |> Option.defaultValue []
104+
105+ let private addContactIfNew (contact: Contact) (contacts: Contact list) =
106+ if contacts |> List.exists (fun c -> c.Pubkey = contact.Pubkey) then contacts
107+ else contacts @ [ contact ]
108+
109+ let private upsertContact (contact: Contact) (contacts: Contact list) =
110+ if contacts |> List.exists (fun c -> c.Pubkey = contact.Pubkey) then
111+ contacts |> List.map (fun c -> if c.Pubkey = contact.Pubkey then contact else c)
112+ else
113+ contacts @ [ contact ]
114+
103115 // ── Init ──
104116
105117 let init () =
@@ -143,11 +155,12 @@ module App =
143155 | None -> { model with Error = Some "No identity generated" }, []
144156
145157 | AuthOk token ->
146− { model with
147− Conn = Online token
148− Page = Conversations
149− Error = None },
150− [ CmdPoll(model.ServerUrl, token, model.PrivKey.Value, model.PollCursor) ]
158+ match model.PrivKey with
159+ | Some priv ->
160+ { model with Conn = Online token; Page = Conversations; Error = None },
161+ [ CmdPoll(model.ServerUrl, token, priv, model.PollCursor) ]
162+ | None ->
163+ { model with Error = Some "No identity generated" }, []
151164
152165 | AuthErr err -> { model with Conn = Offline; Error = Some err }, []
153166
@@ -164,12 +177,12 @@ module App =
164177 Body = text
165178 Timestamp = DateTimeOffset.UtcNow
166179 IsOutgoing = true }
167− let msgs = model.Messages |> Map.tryFind pk |> Option.defaultValue []
180+ let msgs = messagesFor pk model.Messages
168181
169182 { model with
170183 ComposeText = ""
171184 Messages = model.Messages |> Map.add pk (msgs @ [ outMsg ]) },
172− [ CmdSend(model.ServerUrl, token, pk, priv, model.PubKeyHex, pk, text) ]
185+ [ CmdSend(model.ServerUrl, token, pk, priv, text) ]
173186 | _ -> model, []
174187
175188 | Sent(_, status) -> { model with LastSendStatus = status }, []
@@ -178,31 +191,25 @@ module App =
178191 let model' =
179192 incoming
180193 |> List.fold
181− (fun m (fromPk, body, ts, id) ->
182− let msgs = m.Messages |> Map.tryFind fromPk |> Option.defaultValue []
194+ (fun m msg ->
195+ let msgs = messagesFor msg.FromPubkey m.Messages
183196
184− if msgs |> List.exists (fun msg -> msg.Id = id) then
197+ if msgs |> List.exists (fun x -> x.Id = msg.MessageId) then
185198 m
186199 else
187− let newMsg =
188− { Id = id
189− Body = body
190− Timestamp = DateTimeOffset.FromUnixTimeSeconds(ts)
200+ let chatMsg =
201+ { Id = msg.MessageId
202+ Body = msg.Body
203+ Timestamp = DateTimeOffset.FromUnixTimeSeconds(msg.Timestamp)
191204 IsOutgoing = false }
192205
193− let contacts =
194− if m.Contacts |> List.exists (fun c -> c.Pubkey = fromPk) then
195− m.Contacts
196− else
197− m.Contacts @ [ { Pubkey = fromPk; Nickname = truncKey fromPk } ]
198−
199206 { m with
200− Contacts = contacts
201− Messages = m.Messages |> Map.add fromPk (msgs @ [ newMsg ]) })
207+ Contacts = m.Contacts |> addContactIfNew { Pubkey = msg.FromPubkey; Nickname = truncKey msg.FromPubkey }
208+ Messages = m.Messages |> Map.add msg.FromPubkey (msgs @ [ chatMsg ]) })
202209 { model with PollStatus = status; PollCursor = newCursor }
203210
204− match model'.Conn with
205− | Online token -> model', [ CmdPoll(model'.ServerUrl, token, model'.PrivKey.Value, model'.PollCursor) ]
211+ match model'.Conn, model'.PrivKey with
212+ | Online token, Some priv -> model', [ CmdPoll(model'.ServerUrl, token, priv, model'.PollCursor) ]
206213 | _ -> model', []
207214
208215 | DoPoll ->
@@ -219,16 +226,8 @@ module App =
219226 { Pubkey = model.NewPubkey
220227 Nickname = model.NewNickname }
221228
222− let contacts =
223− if model.Contacts |> List.exists (fun c -> c.Pubkey = model.NewPubkey) then
224− model.Contacts
225− |> List.map (fun c ->
226− if c.Pubkey = model.NewPubkey then contact else c)
227− else
228− model.Contacts @ [ contact ]
229−
230229 { model with
231− Contacts = contacts
230+ Contacts = model.Contacts |> upsertContact contact
232231 NewPubkey = ""
233232 NewNickname = ""
234233 Page = Conversations },
@@ -279,11 +278,11 @@ module App =
279278 return AuthErr ex.Message
280279 })
281280
282− | CmdSend(url, token, toHex, privKey, pubKeyHex, recipPubHex, text) ->
281+ | CmdSend(url, token, recipientHex, privKey, text) ->
283282 asyncCmd (async {
284283 try
285− let pubKey = Crypto.fromHex pubKeyHex
286− let recipPub = Crypto.fromHex recipPubHex
284+ let pubKey = privKey.[32..63]
285+ let recipPub = Crypto.fromHex recipientHex
287286 let id = Guid.NewGuid().ToString()
288287 let ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
289288
@@ -292,8 +291,8 @@ module App =
292291
293292 let blob = Crypto.encrypt privKey pubKey recipPub payload
294293 let blobHex = Crypto.toHex blob
295− let! (status, msgId) = ApiClient.sendMessage url token toHex blobHex ts
296− return Sent(toHex, $"{status}:{msgId}")
294+ let! (status, msgId) = ApiClient.sendMessage url token recipientHex blobHex ts
295+ return Sent(recipientHex, $"{status}:{msgId}")
297296 with ex ->
298297 return DismissError // TODO: show send errors properly
299298 })
@@ -326,7 +325,7 @@ module App =
326325 if ptRoot.GetProperty("type").GetString() = "text" then
327326 let msgId = ptRoot.GetProperty("id").GetString()
328327 let body = ptRoot.GetProperty("body").GetString()
329− messages.Add((senderHex, body, ts, msgId))
328+ messages.Add({ FromPubkey = senderHex; Body = body; Timestamp = ts; MessageId = msgId })
330329
331330 ackIds.Add(evtId)
332331 | None ->
@@ -478,7 +477,7 @@ module App =
478477
479478 let private viewChat model pk =
480479 let name = contactName model.Contacts pk
481− let msgs = model.Messages |> Map.tryFind pk |> Option.defaultValue []
480+ let msgs = messagesFor pk model.Messages
482481
483482 ContentPage(
484483 (VStack(spacing = 8.) {
client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll

Binary file not shown.

client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.pdb

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.AssemblyInfo.fs+1−1
@@ -10,7 +10,7 @@ open System.Reflection
1010[<assembly: System.Reflection.AssemblyCompanyAttribute("Skrepka")>]
1111[<assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")>]
1212[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
13−[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+ac8c87f242c436f4a1bad89ff035eecf82a2bb15")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+82fcdc7935484a61c2a22433c36ce8637ad179c8")>]
1414[<assembly: System.Reflection.AssemblyProductAttribute("Skrepka")>]
1515[<assembly: System.Reflection.AssemblyTitleAttribute("Skrepka")>]
1616[<assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")>]
client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.AssemblyInfoInputs.cache+1−1
@@ -1 +1 @@
1−fb7049c87bbc40133ce9fcdb916d1edeb0d2c72163aa8e3476623d121e5e3788
1+0a759b94d305484f3281f9f60b3791a1832d7dde46f8f7a622e4deaf1e156292
client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.pdb

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.sourcelink.json+1−1
@@ -1 +1 @@
1−{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/ac8c87f242c436f4a1bad89ff035eecf82a2bb15/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/82fcdc7935484a61c2a22433c36ce8637ad179c8/*"}}
client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/ref/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/refint/Skrepka.dll

Binary file not shown.

server.knot+22−27
@@ -41,7 +41,7 @@ federationEnabled = not (federationSecret == "")
4141-- Types
4242-- ============================================
4343
44−type PollEvent = {id: Text, eventType: Text, payload: {encryptedBlob: Text, fromKey: Text, timestamp: Timestamp, toKey: Text}}
44+type PollEvent = {id: MessageIdHex, eventType: Text, payload: {encryptedBlob: BlobHex, fromKey: PubkeyHex, timestamp: Timestamp, toKey: PubkeyHex}}
4545
4646type GossipEventType = Text where \s -> s == "online" || s == "offline"
4747
@@ -379,6 +379,12 @@ clampTtl = \ttl -> if ttl < 1 then 1 else if ttl > maxPresenceTtl then maxPresen
379379
380380clampTimestamp = \timestamp t -> if timestamp > t then t else if timestamp < t - messageMaxAge then t - messageMaxAge else timestamp
381381
382+nextSeqTs = \t seqCounter -> if t > seqCounter then t else seqCounter + 1
383+
384+isFederatedOrigin = \originServer -> not (originServer == "") && not (originServer == serverHostN)
385+
386+takeN = \items n -> fold (\acc e -> if count acc >= n then acc else union acc [e]) [] items
387+
382388dedupMessageIds = \messageIds ->
383389 fold (\acc mid ->
384390 let nmid = normalizeHex mid in
@@ -544,14 +550,12 @@ maybePrune = \t -> do
544550 set *lastPrunedAt = t
545551 pruneExpired t
546552
547−sendBatchedGossipToServer : Text -> [{eventType: Text, pubkey: Text, server: Text, timestamp: Timestamp, ttl: Int}] -> IO {network} {}
553+sendBatchedGossipToServer : Text -> [{eventType: GossipEventType, pubkey: Text, server: Text, timestamp: Timestamp, ttl: Int<S>}] -> IO {network} {}
548554sendBatchedGossipToServer = \server allEvents ->
549555 if count allEvents == 0
550556 then yield {}
551557 else do
552− let batch = fold (\acc e ->
553− if count acc >= maxGossipEvents then acc
554− else union acc [e]) [] allEvents
558+ let batch = takeN allEvents maxGossipEvents
555559 let rest = diff allEvents batch
556560 batchResult <- fetch (federationScheme ++ server) (RecvGossip {
557561 authorization: "Bearer " ++ federationSecret,
@@ -561,7 +565,7 @@ sendBatchedGossipToServer = \server allEvents ->
561565 Err _ -> yield {}
562566 sendBatchedGossipToServer server rest
563567
564−sendBatchedGossip : [Text] -> [{eventType: Text, pubkey: Text, server: Text, timestamp: Timestamp, ttl: Int}] -> IO {network} {}
568+sendBatchedGossip : [Text] -> [{eventType: GossipEventType, pubkey: Text, server: Text, timestamp: Timestamp, ttl: Int<S>}] -> IO {network} {}
565569sendBatchedGossip = \peerServers allEvents ->
566570 if count allEvents == 0
567571 then yield {}
@@ -582,7 +586,7 @@ broadcastOfflineGossip = \t peerServers expiredLocal -> do
582586 sendBatchedGossip peerServers gossipEvents
583587 else yield {}
584588
585−retryForwardsForServer : [{encryptedBlob: Text, fromKey: Text, messageId: Text, timestamp: Timestamp, toKey: Text, toServer: Text}] -> Int -> IO {network} {}
589+retryForwardsForServer : [{encryptedBlob: BlobHex, fromKey: PubkeyHex, messageId: MessageIdHex, timestamp: Timestamp, toKey: PubkeyHex, toServer: ServerName}] -> Int -> IO {network} {}
586590retryForwardsForServer = \entries remaining ->
587591 if count entries == 0 || remaining <= 0
588592 then yield {}
@@ -608,21 +612,19 @@ retryForwardsForServer = \entries remaining ->
608612 Err _ -> yield {}
609613 retryForwardsForServer rest (remaining - 1)
610614
611−retryPendingForwards : [{encryptedBlob: Text, fromKey: Text, messageId: Text, timestamp: Timestamp, toKey: Text, toServer: Text}] -> IO {network} {}
615+retryPendingForwards : [{encryptedBlob: BlobHex, fromKey: PubkeyHex, messageId: MessageIdHex, timestamp: Timestamp, toKey: PubkeyHex, toServer: ServerName}] -> IO {network} {}
612616retryPendingForwards = \pending -> do
613617 let servers = collectPeerServers pending (\x -> x.toServer)
614618 sendToEach servers (\server ->
615619 retryForwardsForServer (do x <- pending; where x.toServer == server; yield x) maxForwardRetriesPerServer)
616620 yield {}
617621
618−sendBatchedAcksToServer : [{messageId: Text, toKey: Text, originServer: Text, | a}] -> Text -> IO {network} {}
622+sendBatchedAcksToServer : [{messageId: MessageIdHex, toKey: PubkeyHex, originServer: Text, | a}] -> Text -> IO {network} {}
619623sendBatchedAcksToServer = \serverAcks server ->
620624 if count serverAcks == 0
621625 then yield {}
622626 else do
623− let batch = fold (\acc a ->
624− if count acc >= maxMessageIds then acc
625− else union acc [a]) [] serverAcks
627+ let batch = takeN serverAcks maxMessageIds
626628 let rest = diff serverAcks batch
627629 result <- fetch (federationScheme ++ server) (FederationAck {
628630 authorization: "Bearer " ++ federationSecret,
@@ -639,7 +641,7 @@ sendBatchedAcksToServer = \serverAcks server ->
639641 Err _ -> yield {}
640642 sendBatchedAcksToServer rest server
641643
642−sendAcksAndClean : [{messageId: Text, toKey: Text, originServer: Text, | a}] -> [Text] -> IO {network} {}
644+sendAcksAndClean : [{messageId: MessageIdHex, toKey: PubkeyHex, originServer: Text, | a}] -> [Text] -> IO {network} {}
643645sendAcksAndClean = \pendingAcks servers ->
644646 sendToEach servers (\server ->
645647 sendBatchedAcksToServer (do a <- pendingAcks; where a.originServer == server; yield a) server)
@@ -655,8 +657,7 @@ pruneAndRetry = \t -> do
655657 ackedIds <- *ackedIds
656658 let pendingAcks = do
657659 a <- ackedIds
658− where not (a.originServer == "")
659− where not (a.originServer == serverHostN)
660+ where isFederatedOrigin a.originServer
660661 yield a
661662 yield {expiredLocal: pr.expiredLocal, peerServers: pr.peerServers, pendingForwards: pendingForwards, pendingAcks: pendingAcks}
662663 broadcastOfflineGossip t result.peerServers result.expiredLocal
@@ -832,7 +833,7 @@ serve = \req -> case req of
832833 Just {value: s} -> s.pubkey
833834 let authorized = isJust session
834835 seqCounter <- *seqCounter
835− let seqTs = if t > seqCounter then t else seqCounter + 1
836+ let seqTs = nextSeqTs t seqCounter
836837 let recipientMsgCount = countMessagesFor messages to t
837838 let shouldInsert = authorized && recipientMsgCount < maxMessagesPerRecipient
838839 set *seqCounter = if shouldInsert then seqTs else seqCounter
@@ -858,9 +859,7 @@ serve = \req -> case req of
858859 forwards <- *forwards
859860 let forwardsAtCapacity = count forwards >= maxForwardsEntries
860861 let forwardSlots = maxForwardsEntries - count forwards
861− let forwardServers = fold (\acc s ->
862− if count acc >= forwardSlots then acc
863− else union acc [s]) [] remoteServers
862+ let forwardServers = takeN remoteServers forwardSlots
864863 set *forwards = if shouldInsert && not forwardsAtCapacity && count forwardServers > 0 && federationEnabled
865864 then union forwards (do s <- forwardServers; yield {messageId: msgId, toServer: s, createdAt: t, toKey: to, fromKey: fromKey, encryptedBlob: encryptedBlob, timestamp: clampedTs})
866865 else forwards
@@ -910,8 +909,7 @@ serve = \req -> case req of
910909 Just {value: _} -> diff messages ackedMessages
911910 let ackedFederated = do
912911 m <- ackedMessages
913− where not (m.originServer == "")
914− where not (m.originServer == serverHostN)
912+ where isFederatedOrigin m.originServer
915913 yield {messageId: m.messageId, toKey: m.toKey, ackedAt: t, originServer: m.originServer}
916914 ackedIds <- *ackedIds
917915 let combinedAckedIds = union ackedIds ackedFederated
@@ -925,8 +923,7 @@ serve = \req -> case req of
925923 yield {ok: ok, ackedMessages: ackedMessages, expiredLocal: pr.expiredLocal, peerServers: pr.peerServers, evictedEntries: evictedEntries}
926924 let fedMessages = do
927925 m <- result.ackedMessages
928− where not (m.originServer == "")
929− where not (m.originServer == serverHostN)
926+ where isFederatedOrigin m.originServer
930927 yield m
931928 let originServers = collectPeerServers fedMessages (\m -> m.originServer)
932929 fork (do
@@ -1063,9 +1060,7 @@ serve = \req -> case req of
10631060 where none updatesOnlyAfterOffline (\p -> samePubkeyServer p e)
10641061 yield e
10651062 let reclaimedEntries = if withinCapacity then []
1066− else fold (\acc e ->
1067− if count acc >= reclaimCapacity then acc
1068− else union acc [e]) [] newEntries
1063+ else takeN newEntries reclaimCapacity
10691064 let afterReclaim = if withinCapacity then afterOffline
10701065 else union updatesOnlyAfterOffline reclaimedEntries
10711066 let appliedOnlineUpdates = if withinCapacity then appliedOnline
@@ -1130,7 +1125,7 @@ serve = \req -> case req of
11301125 pr <- maybePrune t
11311126 messages <- *messages
11321127 seqCounter <- *seqCounter
1133− let seqTs = if t > seqCounter then t else seqCounter + 1
1128+ let seqTs = nextSeqTs t seqCounter
11341129 let recipientMsgCount = if not (authorized && validOrigin) then 0
11351130 else countMessagesFor messages toKey t
11361131 let existsInMessages = if not (authorized && validOrigin) then False {}