on master help sign in
Simplify code and improve type safety: remove ForwardInfo, inline computed properties, add ValidGossipEntry, short-circuit validGossipEvents
commitab639f4d659aba03b3f1c1a3c68fa563dca5b433 authoriko <mail@iko.soy> dateFri 10 Apr 2026, 09:09 (6 months ago) parentecdd432
6 files changed+56−82
client/App.fs+24−41 client/Store.fs+2−2 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.sourcelink.json+1−1 server.knot+27−36
client/App.fs+24−41
@@ -13,7 +13,7 @@ module App =
1313
1414 // ── Domain Types ──
1515
16− type Contact = { Pubkey: string; Nickname: string }
16+ type Contact = Store.Contact
1717
1818 type ChatMessage =
1919 { Id: string
@@ -40,7 +40,7 @@ module App =
4040
4141 type ConnState =
4242 | Offline
43− | Connecting
43+ | Connecting of Identity
4444 | Online of Session
4545
4646 type PollResultData =
@@ -82,7 +82,7 @@ module App =
8282 | DoDisconnect
8383 | SetCompose of string
8484 | DoSend
85− | Sent of status: string
85+ | Sent of ApiClient.SendResult
8686 | SetNewPubkey of string
8787 | SetNewNickname of string
8888 | DoSaveContact
@@ -178,17 +178,16 @@ module App =
178178 | DoConnect ->
179179 match model.Identity with
180180 | Some id ->
181− { model with Conn = Connecting }, [ CmdConnect(model.ServerUrl, id) ]
181+ { model with Conn = Connecting id }, [ CmdConnect(model.ServerUrl, id) ]
182182 | None -> { model with Error = Some "No identity generated" }, []
183183
184184 | AuthOk token ->
185− match model.Identity with
186− | Some id ->
185+ match model.Conn with
186+ | Connecting id ->
187187 let session = { Url = model.ServerUrl; Token = token; Identity = id }
188188 { model with Conn = Online session; Page = Conversations; Error = None },
189189 [ CmdPoll(session, model.PollCursor) ]
190− | None ->
191− { model with Error = Some "No identity generated" }, []
190+ | _ -> model, []
192191
193192 | AuthErr err -> { model with Conn = Offline; Error = Some err }, []
194193
@@ -215,7 +214,7 @@ module App =
215214 model', CmdSend(session, pk, text, id) :: saveCmd model'
216215 | _ -> model, []
217216
218− | Sent status -> { model with LastSendStatus = status }, []
217+ | Sent result -> { model with LastSendStatus = $"{result.Status}:{result.MessageId}" }, []
219218
220219 | PollResult { Messages = incoming; Status = status; Cursor = newCursor } ->
221220 let model' =
@@ -286,35 +285,26 @@ module App =
286285 match identityOpt with
287286 | None -> model, []
288287 | Some identity ->
289− let model' = { model with Identity = Some identity; Conn = Connecting }
288+ let model' = { model with Identity = Some identity; Conn = Connecting identity }
290289
291290 let model' =
292291 match dataOpt with
293292 | Some data ->
294− let contacts =
295− data.Contacts
296− |> Array.toList
297− |> List.map (fun c ->
298− { Pubkey = c.Pubkey; Nickname = c.Nickname }: Contact)
299−
300293 let messages =
301294 data.Messages
302− |> Seq.fold
303− (fun acc (KeyValue(pk, dtos)) ->
304− let msgs =
305− dtos
306− |> Array.toList
307− |> List.map (fun m ->
308− { Id = m.Id
309− Body = m.Body
310− Timestamp = DateTimeOffset.FromUnixTimeSeconds m.TimestampUnix
311− IsOutgoing = m.IsOutgoing })
312−
313− acc |> Map.add pk msgs)
314− Map.empty
295+ |> Seq.map (fun (KeyValue(pk, dtos)) ->
296+ pk,
297+ dtos
298+ |> Array.toList
299+ |> List.map (fun m ->
300+ { Id = m.Id
301+ Body = m.Body
302+ Timestamp = DateTimeOffset.FromUnixTimeSeconds m.TimestampUnix
303+ IsOutgoing = m.IsOutgoing }))
304+ |> Map.ofSeq
315305
316306 { model' with
317− Contacts = contacts
307+ Contacts = data.Contacts |> Array.toList
318308 Messages = messages
319309 ServerUrl = if data.ServerUrl <> "" then data.ServerUrl else model.ServerUrl
320310 PollCursor = data.PollCursor
@@ -391,7 +381,7 @@ module App =
391381 let blob = Crypto.encrypt session.Identity.PrivKey recipPub payload
392382 let blobHex = Crypto.toHex blob
393383 let! result = ApiClient.sendMessage session.Url session.Token recipientHex blobHex ts
394− return Sent $"{result.Status}:{result.MessageId}"
384+ return Sent result
395385 with ex ->
396386 return DismissError // TODO: show send errors properly
397387 })
@@ -444,13 +434,6 @@ module App =
444434
445435 | CmdSaveData(contacts, messages, serverUrl, cursor) ->
446436 Cmd.ofEffect (fun _ ->
447− let contactDtos =
448− contacts
449− |> List.map (fun c ->
450− { Store.ContactDto.Pubkey = c.Pubkey
451− Store.ContactDto.Nickname = c.Nickname })
452− |> Array.ofList
453−
454437 let messageDtos = Dictionary<string, Store.MessageDto array>()
455438
456439 messages
@@ -465,7 +448,7 @@ module App =
465448 |> Array.ofList)
466449
467450 Store.saveData
468− { Contacts = contactDtos
451+ { Contacts = contacts |> Array.ofList
469452 Messages = messageDtos
470453 ServerUrl = serverUrl
471454 PollCursor = cursor })
@@ -520,7 +503,7 @@ module App =
520503
521504 match model.Conn with
522505 | Offline -> Button("Connect", DoConnect)
523− | Connecting -> Label("Connecting...").centerTextHorizontal()
506+ | Connecting _ -> Label("Connecting...").centerTextHorizontal()
524507 | Online _ ->
525508 Button("Disconnect", DoDisconnect)
526509 Button("Go to Conversations", Nav Conversations)
@@ -533,7 +516,7 @@ module App =
533516 let connStr =
534517 match model.Conn with
535518 | Offline -> "OFFLINE"
536− | Connecting -> "CONNECTING"
519+ | Connecting _ -> "CONNECTING"
537520 | Online _ -> "ONLINE"
538521
539522 let contactNames =
client/Store.fs+2−2
@@ -9,7 +9,7 @@ open Microsoft.Maui.Storage
99module Store =
1010
1111 [<CLIMutable>]
12− type ContactDto =
12+ type Contact =
1313 { Pubkey: string
1414 Nickname: string }
1515
@@ -22,7 +22,7 @@ module Store =
2222
2323 [<CLIMutable>]
2424 type DataDto =
25− { Contacts: ContactDto array
25+ { Contacts: Contact array
2626 Messages: Dictionary<string, MessageDto array>
2727 ServerUrl: string
2828 PollCursor: uint64 }
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+53111fd5732cdcd3f192c7b234c1e758166a5b4e")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+ecdd432bf2c8310d4626a31121c2e9c307be193e")>]
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−258df1087fc418adab33f09b6403611ecf00ae4643e2b1d8257eaca6be4c82ac
1+83cc1d0f7837e56dfd3e64e2491b55a1e8c0c0e8eac0237e2d9f361c0cdf8383
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/53111fd5732cdcd3f192c7b234c1e758166a5b4e/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/ecdd432bf2c8310d4626a31121c2e9c307be193e/*"}}
server.knot+27−36
@@ -49,6 +49,8 @@ type GossipEventType = Text where \s -> s == "online" || s == "offline"
4949
5050type GossipEvent = {eventType: GossipEventType, pubkey: Text, server: Text, timestamp: Timestamp, ttl: Int<S>}
5151
52+type ValidGossipEntry = {pubkey: PubkeyHex, server: Text, expiresAt: Timestamp, timestamp: Timestamp, ttl: Int<S>}
53+
5254type MessageStatus = Text where \s -> s == "unauthorized" || s == "rejected" || s == "delivered" || s == "federated" || s == "queued"
5355
5456type PubkeyHex = Text where \s -> isValidHex s && not (tooLong s maxPubkeyLen)
@@ -68,7 +70,7 @@ type ForwardEntry = {messageId: MessageIdHex, toServer: ServerName, createdAt: T
6870type AckedEntry = {messageId: MessageIdHex, toKey: PubkeyHex, ackedAt: Timestamp, originServer: ServerName}
6971type AuthResult = {authorized: Bool, pubkey: Text}
7072type PruneResult = {expiredLocal: [PresenceEntry], peerServers: [Text]}
71−type ForwardInfo = {messageId: MessageIdHex, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, toServer: ServerName}
73+
7274
7375-- ============================================
7476-- Persisted State
@@ -326,20 +328,23 @@ isHighOctetPrefix = \sn ->
326328 || (d2 == "4" && isDigitChar d3)
327329 || (d2 == "5" && (d3 == "0" || d3 == "1" || d3 == "2" || d3 == "3" || d3 == "4" || d3 == "5")))
328330
331+matchesHostname = \sn name nameLen ->
332+ take nameLen sn == name && (drop nameLen sn == "" || take 1 (drop nameLen sn) == ":")
333+
329334isBadServerName = \s ->
330335 let sn = toLower (stripTrailingDot s) in
331336 sn == ""
332337 || take 1 sn == "."
333338 || take 1 sn == ":"
334339 || containsBadUrlChar sn
335− || sn == "localhost" || take 10 sn == "localhost:"
336− || sn == "localhost.localdomain" || take 22 sn == "localhost.localdomain:"
340+ || matchesHostname sn "localhost" 9
341+ || matchesHostname sn "localhost.localdomain" 21
337342 || containsLocalhostSuffix sn
338− || sn == "local" || take 6 sn == "local:"
343+ || matchesHostname sn "local" 5
339344 || containsLocalSuffix sn
340− || sn == "internal" || take 9 sn == "internal:"
345+ || matchesHostname sn "internal" 8
341346 || containsInternalSuffix sn
342− || sn == "metadata" || take 9 sn == "metadata:"
347+ || matchesHostname sn "metadata" 8
343348 || take 4 sn == "127." || take 3 sn == "10."
344349 || take 8 sn == "192.168." || take 8 sn == "169.254."
345350 || take 2 sn == "0." || take 1 sn == "["
@@ -489,22 +494,12 @@ countMessagesFor = \messages toKey t ->
489494 where m.receivedAt >= t - messageMaxAge
490495 yield m)
491496
492−&pendingAcks = do
493− ackedIds <- *ackedIds
494− yield (do a <- ackedIds; where isFederatedOrigin a.originServer; yield a)
495−
496−&knownMessageKeys = do
497− messages <- *messages
498− ackedIds <- *ackedIds
499− yield (union
500− (do m <- messages; yield {messageId: m.messageId, toKey: m.toKey})
501− (do a <- ackedIds; yield {messageId: a.messageId, toKey: a.toKey}))
502497
503−validGossipEvents : [GossipEvent] -> Bool -> Timestamp -> GossipEventType -> [{pubkey: PubkeyHex, server: Text, expiresAt: Timestamp, timestamp: Timestamp, ttl: Int<S>}]
498+validGossipEvents : [GossipEvent] -> Bool -> Timestamp -> GossipEventType -> [ValidGossipEntry]
504499validGossipEvents = \events validRequest t eventType ->
505− do
500+ if not validRequest then []
501+ else do
506502 e <- events
507− where validRequest
508503 where e.eventType == eventType
509504 where not (e.server == "")
510505 where not (e.pubkey == "")
@@ -583,10 +578,7 @@ sendBatchedGossipToServer = \server allEvents ->
583578
584579sendBatchedGossip : [Text] -> [GossipEvent] -> IO {network} {}
585580sendBatchedGossip = \peerServers allEvents ->
586− if count allEvents == 0
587− then yield {}
588− else
589− sendToEach peerServers (\server -> sendBatchedGossipToServer server allEvents)
581+ sendToEach peerServers (\server -> sendBatchedGossipToServer server allEvents)
590582
591583broadcastOfflineGossip = \t peerServers expiredLocal -> do
592584 let expiredEntries = mergeByKey (\a b -> a.pubkey == b.pubkey) [] (do
@@ -610,7 +602,7 @@ removeForward = \mid srv -> atomic do
610602 where not (x.messageId == mid && x.toServer == srv)
611603 yield x
612604
613−retryForwardsForServer : [ForwardInfo] -> Int -> IO {network} {}
605+retryForwardsForServer : [ForwardEntry] -> Int -> IO {network} {}
614606retryForwardsForServer = \entries remaining ->
615607 if remaining <= 0
616608 then yield {}
@@ -629,7 +621,7 @@ retryForwardsForServer = \entries remaining ->
629621 Err _ -> yield {}
630622 retryForwardsForServer rest (remaining - 1)
631623
632−retryPendingForwards : [ForwardInfo] -> IO {network} {}
624+retryPendingForwards : [ForwardEntry] -> IO {network} {}
633625retryPendingForwards = \pending -> do
634626 let servers = collectPeerServers pending (\x -> x.toServer)
635627 sendToEach servers (\server ->
@@ -668,10 +660,9 @@ pruneAndRetry = \t -> do
668660 set *lastPrunedAt = t
669661 pr <- pruneExpired t
670662 forwards <- *forwards
671− let pendingForwards = do
672− f <- forwards
673− yield {toServer: f.toServer, toKey: f.toKey, fromKey: f.fromKey, encryptedBlob: f.encryptedBlob, timestamp: f.timestamp, messageId: f.messageId}
674− pendingAcks <- &pendingAcks
663+ let pendingForwards = forwards
664+ ackedIds <- *ackedIds
665+ let pendingAcks = do a <- ackedIds; where isFederatedOrigin a.originServer; yield a
675666 yield {expiredLocal: pr.expiredLocal, peerServers: pr.peerServers, pendingForwards: pendingForwards, pendingAcks: pendingAcks}
676667 broadcastOfflineGossip t result.peerServers result.expiredLocal
677668 if federationEnabled && count result.pendingForwards > 0
@@ -976,7 +967,7 @@ serve = \req -> case req of
976967 where p.pubkey == pubkey
977968 where p.expiresAt > t
978969 yield p
979− let foundServer = if not authorized || count valid == 0
970+ let foundServer = if not authorized
980971 then ""
981972 else if any valid (\p -> p.server == serverHostN)
982973 then serverHostN
@@ -1114,12 +1105,12 @@ serve = \req -> case req of
11141105 messages <- *messages
11151106 seqCounter <- *seqCounter
11161107 let seqTs = nextSeqTs t seqCounter
1117− let recipientMsgCount = if not (authorized && validOrigin) then 0
1118− else countMessagesFor messages toKey t
1119− knownKeys <- &knownMessageKeys
1120− let exists = if not (authorized && validOrigin) then False {}
1121− else any knownKeys (\k -> k.messageId == messageId && k.toKey == toKey)
1122− let shouldInsert = authorized && validOrigin && not exists && recipientMsgCount < maxMessagesPerRecipient
1108+ ackedIds <- *ackedIds
1109+ let exists = authorized && validOrigin
1110+ && (any messages (\m -> m.messageId == messageId && m.toKey == toKey)
1111+ || any ackedIds (\a -> a.messageId == messageId && a.toKey == toKey))
1112+ let shouldInsert = authorized && validOrigin && not exists
1113+ && countMessagesFor messages toKey t < maxMessagesPerRecipient
11231114 set *seqCounter = if shouldInsert then seqTs else seqCounter
11241115 set *messages = if shouldInsert
11251116 then union messages