on master help sign in
Simplify and improve type safety: inline gossip predicates, deduplicate remote servers

- Remove isGossipOnline/isGossipOffline/isGossipUnknown predicates (each used once) - Use case expressions in validGossipEvents and RecvGossip handlers - Fix remoteServers in SendMessage: use collectPeerServers to avoid duplicate forward entries

commit7b4b58d8a12eba7ce8c31b169029ead687ed7426 authoriko <mail@iko.soy> dateSat 11 Apr 2026, 14:03 (6 months ago) parent26d71bf
12 files changed+57−72
client/App.fs+36−50 client/Store.fs+7−6 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+11−13
client/App.fs+36−50
@@ -24,7 +24,7 @@ module App =
2424 | Chat of pubkey: string * compose: string
2525 | AddContact of pubkey: string * nickname: string
2626 | Settings
27− | EditProfile of displayName: string * bio: string * photo: string option
27+ | EditProfile of displayName: string * bio: string * photo: string
2828
2929 type Session = { Url: string; Token: string; Identity: Identity }
3030
@@ -40,7 +40,7 @@ module App =
4040 type DecryptedEvent =
4141 | IncomingChat of senderPubkey: string * message: ChatMessage
4242 | IncomingDeliveryAck of senderPubkey: string * ackIds: string list
43− | IncomingProfile of senderPubkey: string * displayName: string * bio: string * photo: string option
43+ | IncomingProfile of senderPubkey: string * displayName: string * bio: string * photo: string
4444
4545 type PollReceived = { Events: int; Messages: int; Errors: int; FirstError: string option }
4646
@@ -56,7 +56,7 @@ module App =
5656 { Page: Page
5757 Auth: AuthState
5858 ServerUrl: string
59− Contacts: Contact list
59+ Contacts: Map<string, Contact>
6060 PollStatus: PollStatus
6161 Messages: Map<string, ChatMessage list>
6262 Error: string option
@@ -92,7 +92,7 @@ module App =
9292 | SetProfileDisplayName of string
9393 | SetProfileBio of string
9494 | DoPickPhoto
95− | PhotoPicked of string option
95+ | PhotoPicked of string
9696 | DoSaveProfile
9797
9898 // ── CmdMsg ──
@@ -130,10 +130,10 @@ module App =
130130 |> Option.orElseWith (fun () -> Crypto.tryFromHex input)
131131 |> Option.filter (fun bytes -> bytes.Length = 32)
132132
133− let private contactName (contacts: Contact list) (pk: string) =
133+ let private contactName (contacts: Map<string, Contact>) (pk: string) =
134134 let fallback = truncKey pk
135135 contacts
136− |> List.tryFind (fun c -> c.Pubkey = pk)
136+ |> Map.tryFind pk
137137 |> Option.map (fun c ->
138138 if c.Nickname <> fallback then c.Nickname
139139 elif c.DisplayName <> "" then c.DisplayName
@@ -156,15 +156,6 @@ module App =
156156 |> fun updated -> Map.add pk updated messages
157157 | None -> messages
158158
159− let private addContactIfNew (contact: Contact) (contacts: Contact list) =
160− if contacts |> List.exists (fun c -> c.Pubkey = contact.Pubkey) then contacts
161− else contacts @ [ contact ]
162−
163− let private upsertContact (contact: Contact) (contacts: Contact list) =
164− match contacts |> List.tryFindIndex (fun c -> c.Pubkey = contact.Pubkey) with
165− | Some i -> contacts |> List.updateAt i contact
166− | None -> contacts @ [ contact ]
167−
168159 let private newContact pubkey nickname : Contact =
169160 { Pubkey = pubkey; Nickname = nickname; DisplayName = ""; Bio = ""; PhotoBase64 = "" }
170161
@@ -193,7 +184,7 @@ module App =
193184 Auth = NoIdentity
194185 ServerUrl = "http://localhost:8080"
195186 PollStatus = Idle
196− Contacts = []
187+ Contacts = Map.empty
197188 Messages = Map.empty
198189 Error = None
199190 PollCursor = 0L
@@ -281,18 +272,17 @@ module App =
281272 match evt with
282273 | IncomingChat(senderPubkey, message) ->
283274 { m with
284− Contacts = m.Contacts |> addContactIfNew (newContact senderPubkey (truncKey senderPubkey))
275+ Contacts =
276+ if Map.containsKey senderPubkey m.Contacts then m.Contacts
277+ else Map.add senderPubkey (newContact senderPubkey (truncKey senderPubkey)) m.Contacts
285278 Messages = m.Messages |> appendMessage senderPubkey message }
286279 | IncomingDeliveryAck(senderPubkey, ackIds) ->
287280 { m with Messages = m.Messages |> markDelivered senderPubkey ackIds }
288281 | IncomingProfile(senderPubkey, displayName, bio, photo) ->
289282 { m with
290283 Contacts =
291− m.Contacts
292− |> List.map (fun c ->
293− if c.Pubkey = senderPubkey then
294− { c with DisplayName = displayName; Bio = bio; PhotoBase64 = photo |> Option.defaultValue "" }
295− else c) })
284+ m.Contacts |> Map.change senderPubkey (Option.map (fun c ->
285+ { c with DisplayName = displayName; Bio = bio; PhotoBase64 = photo })) })
296286 { model with PollStatus = status; PollCursor = newCursor }
297287
298288 model',
@@ -307,15 +297,14 @@ module App =
307297 match tryParsePubkey cpk with
308298 | Some bytes ->
309299 let hex = Crypto.toHex bytes
310− let existing = model.Contacts |> List.tryFind (fun c -> c.Pubkey = hex)
311− let contact: Contact =
312− match existing with
300+ let contact =
301+ match Map.tryFind hex model.Contacts with
313302 | Some e -> { e with Nickname = cnn }
314303 | None -> newContact hex cnn
315304
316305 let model' =
317306 { model with
318− Contacts = model.Contacts |> upsertContact contact
307+ Contacts = Map.add hex contact model.Contacts
319308 Page = Conversations }
320309
321310 model', [ saveCmdMsg model' ]
@@ -353,7 +342,7 @@ module App =
353342 { model with Profile = Some profile; Page = Settings },
354343 [ CmdSaveProfile profile
355344 match trySession model with
356− | Some session -> CmdSendProfileToAll(session, profile, model.Contacts |> List.map _.Pubkey)
345+ | Some session -> CmdSendProfileToAll(session, profile, model.Contacts.Keys |> Seq.toList)
357346 | None -> () ]
358347 | _ -> model, []
359348
@@ -385,7 +374,7 @@ module App =
385374 type private Envelope =
386375 | TextMessage of id: string * body: string
387376 | DeliveryAck of id: string * ackIds: string list
388− | ProfileMessage of id: string * displayName: string * bio: string * photo: string option
377+ | ProfileMessage of id: string * displayName: string * bio: string * photo: string
389378
390379 let private serializeEnvelope = function
391380 | TextMessage(id, body) ->
@@ -393,7 +382,7 @@ module App =
393382 | DeliveryAck(id, ackIds) ->
394383 JsonSerializer.Serialize({| ``type`` = "delivery.ack"; id = id; ack_ids = ackIds |})
395384 | ProfileMessage(id, displayName, bio, photo) ->
396− JsonSerializer.Serialize({| ``type`` = "profile"; id = id; timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); display_name = displayName; bio = bio; photo = (photo |> Option.defaultValue null) |})
385+ JsonSerializer.Serialize({| ``type`` = "profile"; id = id; timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); display_name = displayName; bio = bio; photo = photo |})
397386
398387 let private sendEnvelope (session: Session) (recipientHex: string) (envelope: Envelope) =
399388 async {
@@ -420,7 +409,7 @@ module App =
420409 | "profile" ->
421410 let displayName = root.GetProperty("display_name").GetString()
422411 let bio = tryGetJsonString root "bio" |> Option.defaultValue ""
423− let photo = tryGetJsonString root "photo"
412+ let photo = tryGetJsonString root "photo" |> Option.defaultValue ""
424413 Some(ProfileMessage(root.GetProperty("id").GetString(), displayName, bio, photo))
425414 | _ -> None
426415
@@ -487,17 +476,15 @@ module App =
487476 let! response = poll session.Url session.Token cursor
488477 let ackIds = response.Events |> Array.map _.Id |> Array.toList
489478
490− let events, errors =
491− Array.foldBack
492− (fun (evt: PollEvent) (evts, errs) ->
493− match evt.EventType with
494− | Message ->
495− match decryptEvent session.Identity.PrivKey evt.Payload with
496− | Ok event -> (event :: evts, errs)
497− | Error err -> (evts, err :: errs)
498− | UnknownEvent _ -> (evts, errs))
499− response.Events
500− ([], [])
479+ let results =
480+ response.Events
481+ |> Array.choose (fun evt ->
482+ match evt.EventType with
483+ | Message -> Some(decryptEvent session.Identity.PrivKey evt.Payload)
484+ | UnknownEvent _ -> None)
485+
486+ let events = results |> Array.choose (function Ok e -> Some e | _ -> None) |> Array.toList
487+ let errors = results |> Array.choose (function Error e -> Some e | _ -> None) |> Array.toList
501488
502489 if not ackIds.IsEmpty then
503490 try do! ackMessages session.Url session.Token ackIds
@@ -544,14 +531,14 @@ module App =
544531 try
545532 let! results = Microsoft.Maui.Media.MediaPicker.Default.PickPhotosAsync() |> Async.AwaitTask
546533 match results |> Seq.tryHead with
547− | None -> return PhotoPicked None
534+ | None -> return PhotoPicked ""
548535 | Some file ->
549536 use! stream = file.OpenReadAsync() |> Async.AwaitTask
550537 use ms = new MemoryStream()
551538 do! stream.CopyToAsync(ms) |> Async.AwaitTask
552− return PhotoPicked(Some(Convert.ToBase64String(ms.ToArray())))
539+ return PhotoPicked(Convert.ToBase64String(ms.ToArray()))
553540 with _ ->
554− return PhotoPicked None
541+ return PhotoPicked ""
555542 })
556543
557544 | CmdSaveProfile profile ->
@@ -623,7 +610,7 @@ module App =
623610 let displayName, bio, photo =
624611 match model.Profile with
625612 | Some p -> p.DisplayName, p.Bio, p.PhotoBase64
626− | None -> "", "", None
613+ | None -> "", "", ""
627614 if displayName <> "" then
628615 Label($"Name: {displayName}").font(size = 14.)
629616 if bio <> "" then
@@ -673,7 +660,7 @@ module App =
673660 .centerTextHorizontal()
674661 .textColor(Colors.Gray)
675662
676− for c in model.Contacts do
663+ for c in model.Contacts.Values do
677664 let preview =
678665 messagesFor c.Pubkey model.Messages
679666 |> List.tryLast
@@ -759,13 +746,12 @@ module App =
759746
760747 viewErrorBanner model.Error
761748
762− match photo with
763− | Some p when p <> "" ->
764− Image(new MemoryStream(Convert.FromBase64String(p)) :> Stream)
749+ if photo <> "" then
750+ Image(new MemoryStream(Convert.FromBase64String(photo)) :> Stream)
765751 .width(120.)
766752 .height(120.)
767753 .centerHorizontal()
768− | _ ->
754+ else
769755 Label("No Photo")
770756 .font(size = 16.)
771757 .centerTextHorizontal()
client/Store.fs+7−6
@@ -18,7 +18,7 @@ module Store =
1818 type Profile =
1919 { DisplayName: string
2020 Bio: string
21− PhotoBase64: string option }
21+ PhotoBase64: string }
2222
2323 [<RequireQualifiedAccess>]
2424 type DeliveryStatus = Sent | Delivered
@@ -31,7 +31,7 @@ module Store =
3131 Status: DeliveryStatus }
3232
3333 type Data =
34− { Contacts: Contact list
34+ { Contacts: Map<string, Contact>
3535 Messages: Map<string, ChatMessage list>
3636 ServerUrl: string option
3737 PollCursor: int64 }
@@ -93,7 +93,7 @@ module Store =
9393 |> Option.map (fun p ->
9494 { DisplayName = p.DisplayName |> Option.ofObj |> Option.defaultValue ""
9595 Bio = p.Bio |> Option.ofObj |> Option.defaultValue ""
96− PhotoBase64 = p.PhotoBase64 |> Option.ofObj })
96+ PhotoBase64 = p.PhotoBase64 |> Option.ofObj |> Option.defaultValue "" })
9797 with _ -> None
9898
9999 let saveProfile (profile: Profile) =
@@ -103,7 +103,7 @@ module Store =
103103 { Id = "me"
104104 DisplayName = profile.DisplayName
105105 Bio = profile.Bio
106− PhotoBase64 = profile.PhotoBase64 |> Option.defaultValue null }
106+ PhotoBase64 = profile.PhotoBase64 }
107107 |> ignore
108108 with _ -> ()
109109
@@ -122,7 +122,8 @@ module Store =
122122 DisplayName = c.DisplayName |> Option.ofObj |> Option.defaultValue ""
123123 Bio = c.Bio |> Option.ofObj |> Option.defaultValue ""
124124 PhotoBase64 = c.PhotoBase64 |> Option.ofObj |> Option.defaultValue "" })
125− |> Seq.toList
125+ |> Seq.map (fun c -> c.Pubkey, c)
126+ |> Map.ofSeq
126127
127128 let messages =
128129 db.GetCollection<MessageDoc>("messages").FindAll()
@@ -145,7 +146,7 @@ module Store =
145146 let messages = db.GetCollection<MessageDoc>("messages")
146147 let settings = db.GetCollection<SettingsDoc>("settings")
147148
148− for c in data.Contacts do
149+ for c in data.Contacts.Values do
149150 contacts.Upsert c |> ignore
150151
151152 data.Messages
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+50200f4e8cc7b94c2acf338b402efe56d4dce414")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+26d71bfd7b4764c7381338d2d47a0b86c18d9a9b")>]
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−8af8c70485fb6088d5df97530926c75e310135762195922457d0a7e470636584
1+ce21a6c11004b9877da85245c10ed4a19bfe33da82c2f55d2943c6c7b8845665
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/50200f4e8cc7b94c2acf338b402efe56d4dce414/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/26d71bfd7b4764c7381338d2d47a0b86c18d9a9b/*"}}
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+11−13
@@ -70,10 +70,6 @@ impl FromJSON GossipEventType where
7070 else if s == "offline" then GossipOffline {}
7171 else GossipUnknown {}
7272
73−isGossipOnline = \et -> case et of GossipOnline {} -> True {}; _ -> False {}
74−isGossipOffline = \et -> case et of GossipOffline {} -> True {}; _ -> False {}
75−isGossipUnknown = \et -> case et of GossipUnknown {} -> True {}; _ -> False {}
76−
7773type GossipEvent = {eventType: GossipEventType, pubkey: PubkeyHex, server: ServerName, timestamp: Timestamp, ttl: Int<S>}
7874
7975type ValidGossipEntry = {eventType: GossipEventType, pubkey: PubkeyHex, server: ServerName, expiresAt: Timestamp, timestamp: Timestamp, ttl: Int<S>}
@@ -523,12 +519,14 @@ countMessagesFor = \messages toKey t ->
523519validGossipEvents : [GossipEvent] -> Timestamp -> [ValidGossipEntry]
524520validGossipEvents = \events t -> do
525521 e <- events
526− where not (isGossipUnknown e.eventType)
527− where e.server != serverHostN && e.timestamp <= t + clockSkewTolerance
528− let ttl = clampTtl e.ttl
529− let expiresAt = e.timestamp + ttl * 1000
530− where expiresAt > t
531− yield {eventType: e.eventType, pubkey: e.pubkey, server: e.server, expiresAt: expiresAt, timestamp: e.timestamp, ttl: ttl}
522+ case e.eventType of
523+ GossipUnknown {} -> []
524+ _ -> do
525+ where e.server != serverHostN && e.timestamp <= t + clockSkewTolerance
526+ let ttl = clampTtl e.ttl
527+ let expiresAt = e.timestamp + ttl * 1000
528+ where expiresAt > t
529+ yield {eventType: e.eventType, pubkey: e.pubkey, server: e.server, expiresAt: expiresAt, timestamp: e.timestamp, ttl: ttl}
532530
533531sendToEach : [ServerName] -> (ServerName -> (a {})) -> a {}
534532sendToEach = \servers action ->
@@ -850,7 +848,7 @@ serve = \req -> case req of
850848 encryptedBlob: encryptedBlob, timestamp: clampedTs, origin: LocalOrigin {}, receivedAt: seqTs}]
851849 let toPresence = do p <- presence; where p.pubkey == to && p.expiresAt > t; yield p
852850 let localOnline = any toPresence (\p -> p.server == serverHostN)
853− let remoteServers = do p <- toPresence; where p.server != serverHostN; yield p.server
851+ let remoteServers = collectPeerServers toPresence (\p -> p.server)
854852 forwards <- *forwards
855853 let forwardServers = takeN remoteServers (maxForwardsEntries - count forwards)
856854 let shouldForward = federationEnabled && count forwardServers > 0
@@ -950,8 +948,8 @@ serve = \req -> case req of
950948 then rejectFederation t respond
951949 else do
952950 let allValidEntries = validGossipEvents events t
953− let onlineEntries = do e <- allValidEntries; where isGossipOnline e.eventType; yield e
954− let trustedOffline = do e <- allValidEntries; where isGossipOffline e.eventType && e.server == fromServer; yield e
951+ let onlineEntries = do e <- allValidEntries; case e.eventType of GossipOnline {} -> yield e; _ -> []
952+ let trustedOffline = do e <- allValidEntries; case e.eventType of GossipOffline {} -> do where e.server == fromServer; yield e; _ -> []
955953 let filteredOnline = resolveByTimestamp onlineEntries trustedOffline
956954 let filteredOffline = resolveByTimestamp trustedOffline onlineEntries
957955 let deduped = mergeByPubkeyServer [] filteredOnline