on master help sign in
Improve type safety and simplify server: consolidate SendResult variants, remove LookupPresence fold field, reuse trustedOffline
commite3148dde3e9c36905f2c3f85861acc4776bc1da2 authoriko <mail@iko.soy> dateFri 10 Apr 2026, 20:16 (6 months ago) parente6d5254
12 files changed+52−56
client/ApiClient.fs+35−38 client/App.fs+3−3 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−12
client/ApiClient.fs+35−38
@@ -12,12 +12,46 @@ module ApiClient =
1212 [<CLIMutable>]
1313 type EventPayload = { EncryptedBlob: string; Timestamp: int64 }
1414
15+ [<JsonConverter(typeof<EventTypeConverter>)>]
1516 type EventType = Message | Ack | UnknownEvent of string
17+ and private EventTypeConverter() =
18+ inherit JsonConverter<EventType>()
19+ override _.Read(reader, _, _) =
20+ match reader.GetString() with
21+ | "message" -> Message
22+ | "ack" -> Ack
23+ | s -> UnknownEvent s
24+ override _.Write(writer, value, _) =
25+ writer.WriteStringValue(
26+ match value with
27+ | Message -> "message"
28+ | Ack -> "ack"
29+ | UnknownEvent s -> s)
1630
1731 [<CLIMutable>]
1832 type PollEvent = { Id: string; EventType: EventType; Payload: EventPayload }
1933
34+ [<JsonConverter(typeof<MessageStatusConverter>)>]
2035 type MessageStatus = Delivered | Federated | Queued | Rejected | Unauthorized | UnknownStatus of string
36+ and private MessageStatusConverter() =
37+ inherit JsonConverter<MessageStatus>()
38+ override _.Read(reader, _, _) =
39+ match reader.GetString() with
40+ | "delivered" -> Delivered
41+ | "federated" -> Federated
42+ | "queued" -> Queued
43+ | "rejected" -> Rejected
44+ | "unauthorized" -> Unauthorized
45+ | s -> UnknownStatus s
46+ override _.Write(writer, value, _) =
47+ writer.WriteStringValue(
48+ match value with
49+ | Delivered -> "delivered"
50+ | Federated -> "federated"
51+ | Queued -> "queued"
52+ | Rejected -> "rejected"
53+ | Unauthorized -> "unauthorized"
54+ | UnknownStatus s -> s)
2155
2256 [<CLIMutable>]
2357 type private SendResult = { Status: MessageStatus }
@@ -41,45 +75,8 @@ module ApiClient =
4175 else ok t.Result)
4276 |> ignore)
4377
44− type private MessageStatusConverter() =
45− inherit JsonConverter<MessageStatus>()
46− override _.Read(reader, _, _) =
47− match reader.GetString() with
48− | "delivered" -> Delivered
49− | "federated" -> Federated
50− | "queued" -> Queued
51− | "rejected" -> Rejected
52− | "unauthorized" -> Unauthorized
53− | s -> UnknownStatus s
54− override _.Write(writer, value, _) =
55− writer.WriteStringValue(
56− match value with
57− | Delivered -> "delivered"
58− | Federated -> "federated"
59− | Queued -> "queued"
60− | Rejected -> "rejected"
61− | Unauthorized -> "unauthorized"
62− | UnknownStatus s -> s)
63−
64− type private EventTypeConverter() =
65− inherit JsonConverter<EventType>()
66− override _.Read(reader, _, _) =
67− match reader.GetString() with
68− | "message" -> Message
69− | "ack" -> Ack
70− | s -> UnknownEvent s
71− override _.Write(writer, value, _) =
72− writer.WriteStringValue(
73− match value with
74− | Message -> "message"
75− | Ack -> "ack"
76− | UnknownEvent s -> s)
77−
7878 let private jsonOpts =
79− let opts = JsonSerializerOptions(PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString)
80− opts.Converters.Add(MessageStatusConverter())
81− opts.Converters.Add(EventTypeConverter())
82− opts
79+ JsonSerializerOptions(PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString)
8380
8481 let private client =
8582 let c = new HttpClient()
client/App.fs+3−3
@@ -319,7 +319,7 @@ module App =
319319
320320 match Crypto.decrypt privKey blob with
321321 | Some(plaintext, senderHex) ->
322− let envelope = JsonSerializer.Deserialize<TextEnvelope>(plaintext, envelopeJsonOpts)
322+ let envelope = JsonSerializer.Deserialize<TextEnvelope>(plaintext)
323323
324324 match envelope.Type with
325325 | Text ->
@@ -336,7 +336,7 @@ module App =
336336 let mapCmd cmdMsg =
337337 match cmdMsg with
338338 | CmdGenIdentity ->
339− asyncCmd (async { return IdentityReady(Crypto.generateIdentity ()) })
339+ Cmd.ofEffect (fun dispatch -> dispatch (IdentityReady(Crypto.generateIdentity ())))
340340
341341 | CmdConnect(url, identity) ->
342342 asyncCmd (async {
@@ -353,7 +353,7 @@ module App =
353353 try
354354 let recipPub = Crypto.fromHex recipientHex
355355 let ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
356− let payload = JsonSerializer.Serialize<TextEnvelope>({ Type = Text; Id = messageId; Body = text }, envelopeJsonOpts)
356+ let payload = JsonSerializer.Serialize<TextEnvelope>({ Type = Text; Id = messageId; Body = text })
357357 let blob = Crypto.encrypt session.Identity.PrivKey recipPub payload
358358 let! status = sendMessage session.Url session.Token recipientHex (Crypto.toHex blob) ts
359359
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+a6088f11f8b9f333baecd765cfde004d371fea16")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+e6d52540f7a36ceb966de06191cfd7c3d223c800")>]
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−c792c6af7fcf4a9f3bc9ffab40c693a5335abf2b4bd4c1fd73018e15acd8228b
1+35309a0fe3ed35bcd80b2e755c52f81fc42b7e6e3c07f1b3a7cf0e54f7919e32
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/a6088f11f8b9f333baecd765cfde004d371fea16/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/e6d52540f7a36ceb966de06191cfd7c3d223c800/*"}}
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−12
@@ -115,7 +115,7 @@ type PresenceEntry = {pubkey: PubkeyHex, server: AnyServerName, expiresAt: Times
115115type ForwardEntry = {messageId: MessageIdHex, toServer: ServerName, createdAt: Timestamp, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp}
116116type AckedEntry = {messageId: MessageIdHex, toKey: PubkeyHex, ackedAt: Timestamp, originServer: ServerName}
117117type AuthResult = Authorized {pubkey: PubkeyHex} | Unauthorized {}
118−type SendResult = SendUnauthorized {} | SendRejected {} | SendDone {status: MessageStatus, remoteServers: [ServerName], fromKey: PubkeyHex}
118+type SendResult = SendFailed {status: MessageStatus} | SendDone {status: MessageStatus, remoteServers: [ServerName], fromKey: PubkeyHex}
119119
120120
121121-- ============================================
@@ -818,11 +818,11 @@ serve = \req -> case req of
818818 auth <- authenticateRequest authorization t
819819 case auth of
820820 Unauthorized {} ->
821− yield {pr: pr, send: SendUnauthorized {}}
821+ yield {pr: pr, send: SendFailed {status: MsUnauthorized {}}}
822822 Authorized {pubkey: fromKey} -> do
823823 messages <- *messages
824824 if countMessagesFor messages to t >= maxMessagesPerRecipient
825− then yield {pr: pr, send: SendRejected {}}
825+ then yield {pr: pr, send: SendFailed {status: MsRejected {}}}
826826 else do
827827 presence <- *presence
828828 seqCounter <- *seqCounter
@@ -860,8 +860,7 @@ serve = \req -> case req of
860860 _ -> yield {}
861861 broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
862862 yield (case result.send of
863− SendUnauthorized _ -> respond {status: MsUnauthorized {}, messageId: ""}
864− SendRejected _ -> respond {status: MsRejected {}, messageId: ""}
863+ SendFailed {status} -> respond {status: status, messageId: ""}
865864 SendDone {status} -> respond {status: status, messageId: msgId})
866865
867866 AckMessages {messageIds, authorization, respond} -> do
@@ -913,10 +912,10 @@ serve = \req -> case req of
913912 where p.pubkey == pubkey && p.expiresAt > t
914913 yield p
915914 let foundServer = (fold (\acc p ->
916− if acc.local then acc
917− else if p.server == serverHostN then {server: serverHostN, best: 0, local: True {}}
918− else if p.expiresAt > acc.best then {server: p.server, best: p.expiresAt, local: False {}}
919− else acc) {server: "", best: 0, local: False {}} valid).server
915+ if acc.server == serverHostN then acc
916+ else if p.server == serverHostN then {server: serverHostN, best: 0}
917+ else if p.expiresAt > acc.best then {server: p.server, best: p.expiresAt}
918+ else acc) {server: "", best: 0} valid).server
920919 yield {pr: pr, authorized: True {}, foundServer: foundServer}
921920 forkBroadcastOffline t result.pr
922921 yield (respond {online: result.foundServer != "", server: result.foundServer, authorized: result.authorized})
@@ -932,16 +931,16 @@ serve = \req -> case req of
932931 else do
933932 let allValidEntries = validGossipEvents events t
934933 let onlineEntries = do e <- allValidEntries; where isGossipOnline e.eventType; yield e
935− let offlineEntries = do e <- allValidEntries; where not (isGossipOnline e.eventType); yield e
934+ let offlineEntries = diff allValidEntries onlineEntries
935+ let trustedOffline = do e <- offlineEntries; where e.server == fromServer; yield e
936936 let filteredOnline = do
937937 e <- onlineEntries
938− where none offlineEntries (\o -> o.server == fromServer && samePubkeyServer o e && o.timestamp > e.timestamp)
938+ where none trustedOffline (\o -> samePubkeyServer o e && o.timestamp > e.timestamp)
939939 yield e
940940 let filteredOffline = do
941941 e <- offlineEntries
942942 where none onlineEntries (\o -> samePubkeyServer o e && o.timestamp > e.timestamp)
943943 yield e
944− let trustedOffline = do e <- offlineEntries; where e.server == fromServer; yield e
945944 let deduped = mergeByPubkeyServer [] filteredOnline
946945 gossipResult <- atomic do
947946 pr <- maybePrune t