on master help sign in
Simplify code and improve type safety: separate form input state from Page DU, add dedicated input messages for compose and contact form fields
commite7abeea926015538a30f3346744ca1635404a646 authoriko <mail@iko.soy> dateFri 10 Apr 2026, 18:35 (6 months ago) parent5a91ff1
29 files changed+113−67
client/App.fs+37−18 client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/Skrepka.dll client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/Skrepka.pdb client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/_CodeSignature/CodeResources+4−4 client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/MonoBundle/Skrepka.dll client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/MonoBundle/Skrepka.pdb client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/_CodeSignature/CodeResources+2−2 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-ios/iossimulator-arm64/Skrepka.AssemblyInfo.fs+1−1 client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.AssemblyInfoInputs.cache+1−1 client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.fsproj.CoreCompileInputs.cache+1−1 client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.fsproj.FileListAbsolute.txt+23 client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.sourcelink.json+1−1 client/obj/Debug/net10.0-ios/iossimulator-arm64/ref/Skrepka.dll client/obj/Debug/net10.0-ios/iossimulator-arm64/refint/Skrepka.dll 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.fsproj.CoreCompileInputs.cache+1−1 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+39−35
client/App.fs+37−18
@@ -45,8 +45,8 @@ module App =
4545 type Page =
4646 | Setup
4747 | Conversations
48− | Chat of pubkey: string * compose: string
49− | AddContact of pubkey: string * nickname: string
48+ | Chat of pubkey: string
49+ | AddContact
5050 | Settings
5151
5252 type Session = { Url: string; Token: string; Identity: Identity }
@@ -70,12 +70,18 @@ module App =
7070 PollStatus: string
7171 Messages: Map<string, ChatMessage list>
7272 Error: string option
73− PollCursor: int64 }
73+ PollCursor: int64
74+ Compose: string
75+ ContactPubkey: string
76+ ContactNickname: string }
7477
7578 // ── Msg ──
7679
7780 type Msg =
7881 | SetPage of Page
82+ | SetCompose of string
83+ | SetContactPubkey of string
84+ | SetContactNickname of string
7985 | GenIdentity
8086 | IdentityReady of Identity
8187 | SetServerUrl of string
@@ -175,15 +181,26 @@ module App =
175181 Contacts = []
176182 Messages = Map.empty
177183 Error = None
178− PollCursor = 0L },
184+ PollCursor = 0L
185+ Compose = ""
186+ ContactPubkey = ""
187+ ContactNickname = "" },
179188 [ CmdLoadState ]
180189
181190 // ── Update ──
182191
183192 let update msg model =
184193 match msg with
194+ | SetPage(Chat pk) ->
195+ { model with Page = Chat pk; Compose = "" }, []
196+ | SetPage AddContact ->
197+ { model with Page = AddContact; ContactPubkey = ""; ContactNickname = "" }, []
185198 | SetPage page -> { model with Page = page }, []
186199
200+ | SetCompose text -> { model with Compose = text }, []
201+ | SetContactPubkey pk -> { model with ContactPubkey = pk }, []
202+ | SetContactNickname nn -> { model with ContactNickname = nn }, []
203+
187204 | GenIdentity -> model, [ CmdGenIdentity ]
188205
189206 | IdentityReady identity ->
@@ -212,20 +229,21 @@ module App =
212229
213230 | DoSend ->
214231 match model.Page, trySession model with
215− | Chat(pk, compose), Some session when compose <> "" ->
232+ | Chat pk, Some session when model.Compose <> "" ->
233+ let text = model.Compose
216234 let id = Guid.NewGuid().ToString()
217235 let outMsg =
218236 { Id = id
219− Body = compose
237+ Body = text
220238 Timestamp = DateTimeOffset.UtcNow
221239 IsOutgoing = true }
222240
223241 let model' =
224242 { model with
225− Page = Chat(pk, "")
243+ Compose = ""
226244 Messages = model.Messages |> appendMessage pk outMsg }
227245
228− model', [ CmdSend(session, pk, compose, id); saveCmdMsg model' ]
246+ model', [ CmdSend(session, pk, text, id); saveCmdMsg model' ]
229247 | _ -> model, []
230248
231249 | SendFailed err -> { model with Error = Some err }, []
@@ -247,8 +265,9 @@ module App =
247265 if not incoming.IsEmpty then saveCmdMsg model' ]
248266
249267 | DoSaveContact ->
268+ let pk, nn = model.ContactPubkey, model.ContactNickname
250269 match model.Page with
251− | AddContact(pk, nn) when pk <> "" && nn <> "" ->
270+ | AddContact when pk <> "" && nn <> "" ->
252271 match Phonemic.fromOb pk |> Option.orElseWith (fun () -> Crypto.tryFromHex pk) with
253272 | Some bytes when bytes.Length = 32 ->
254273 let contact: Contact = { Pubkey = Crypto.toHex bytes; Nickname = nn }
@@ -482,7 +501,7 @@ module App =
482501 Label("Conversations")
483502 .font(size = 24.)
484503
485− Button("+", SetPage(AddContact("", "")))
504+ Button("+", SetPage AddContact)
486505 Button("Settings", SetPage Settings)
487506 }
488507
@@ -511,13 +530,13 @@ module App =
511530 |> Option.map (fun m -> truncate 40 m.Body)
512531 |> Option.defaultValue ""
513532
514− Button($"{c.Nickname}\n{preview}", SetPage(Chat(c.Pubkey, "")))
533+ Button($"{c.Nickname}\n{preview}", SetPage(Chat c.Pubkey))
515534 })
516535 .padding(20.)
517536 )
518537 )
519538
520− let private viewChat model pk compose =
539+ let private viewChat model pk =
521540 let name = contactName model.Contacts pk
522541 let msgs = messagesFor pk model.Messages
523542
@@ -549,7 +568,7 @@ module App =
549568 .font(size = 14.)
550569
551570 HStack(spacing = 8.) {
552− Entry(compose, fun t -> SetPage(Chat(pk, t)))
571+ Entry(model.Compose, SetCompose)
553572 .placeholder("Message...")
554573
555574 Button("Send", DoSend)
@@ -558,7 +577,7 @@ module App =
558577 .padding(12.)
559578 )
560579
561− let private viewAddContact pk nn =
580+ let private viewAddContact model =
562581 ContentPage(
563582 (VStack(spacing = 16.) {
564583 HStack(spacing = 8.) {
@@ -569,11 +588,11 @@ module App =
569588 }
570589
571590 Label("Public Key:")
572− Entry(pk, fun t -> SetPage(AddContact(t, nn)))
591+ Entry(model.ContactPubkey, SetContactPubkey)
573592 .placeholder("sampel-palnet-...")
574593
575594 Label("Nickname:")
576− Entry(nn, fun t -> SetPage(AddContact(pk, t)))
595+ Entry(model.ContactNickname, SetContactNickname)
577596
578597 Button("Save Contact", DoSaveContact)
579598 .centerHorizontal()
@@ -587,8 +606,8 @@ module App =
587606 | Setup -> viewSetup ()
588607 | Settings -> viewSettings model
589608 | Conversations -> viewConversations model
590− | Chat(pk, compose) -> viewChat model pk compose
591− | AddContact(pk, nn) -> viewAddContact pk nn
609+ | Chat pk -> viewChat model pk
610+ | AddContact -> viewAddContact model
592611
593612 Application() { Window(page) }
594613
client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/Skrepka.dll

Binary file not shown.

client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/Skrepka.pdb

Binary file not shown.

client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/_CodeSignature/CodeResources+4−4
@@ -166,11 +166,11 @@
166166 </data>
167167 <key>Skrepka.dll</key>
168168 <data>
169− lezBPlMGgvvXPLrij5BGxw+mSWU=
169+ odqTbduEhGM/hvZvmJSmPAW/DxQ=
170170 </data>
171171 <key>Skrepka.pdb</key>
172172 <data>
173− GEoIFmupvmKvjt37aNX0Nqhf4Mw=
173+ 6ZCzYSeKwCxHL9bsITOsUC1FbSY=
174174 </data>
175175 <key>Sodium.Core.dll</key>
176176 <data>
@@ -1393,14 +1393,14 @@
13931393 <dict>
13941394 <key>hash2</key>
13951395 <data>
1396− lOwwpy7OvL/VnkdUcEg8WtWpvQYej7RHx+vgMXJ9cIk=
1396+ 2y12o1alIca/RdSLuaoUEAbdYM1HImAE66/FLxG9NZ0=
13971397 </data>
13981398 </dict>
13991399 <key>Skrepka.pdb</key>
14001400 <dict>
14011401 <key>hash2</key>
14021402 <data>
1403− jNIcynqsu5mNKMdxE4ue6lbnHnYvri+aDBLPMWQYh38=
1403+ jCeuDQPnhCUmzAqkbCUw8pzbweZFd3m0luVDHGR5lBs=
14041404 </data>
14051405 </dict>
14061406 <key>Sodium.Core.dll</key>
client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll

Binary file not shown.

client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb

Binary file not shown.

client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/MonoBundle/Skrepka.dll

Binary file not shown.

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

Binary file not shown.

client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/_CodeSignature/CodeResources+2−2
@@ -251,14 +251,14 @@
251251 <dict>
252252 <key>hash2</key>
253253 <data>
254− mCT+MjgJCiZB3ntslUAZlKSRDaSra+agyPLYQVKRysE=
254+ km4jJeyq8/Komz5joA4/Gcm8bUYB7zg5nUoDcC4qySM=
255255 </data>
256256 </dict>
257257 <key>MonoBundle/Skrepka.pdb</key>
258258 <dict>
259259 <key>hash2</key>
260260 <data>
261− RjcWvTe0sRpmh7W0SPeKoUI1aVen35HuEM/X+//ONNk=
261+ iSV2W540FfQSaG7HhcVDuQi7W3VeL2x8ALd8CmMz+BE=
262262 </data>
263263 </dict>
264264 <key>MonoBundle/Sodium.Core.dll</key>
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-ios/iossimulator-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+5c34fab63923cdb1b780a7e5a5ce5fae72adf78e")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+5a91ff165d267f1b48cd27804ddc67fbfbb1a12b")>]
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-ios/iossimulator-arm64/Skrepka.AssemblyInfoInputs.cache+1−1
@@ -1 +1 @@
1−5ce28345d4f1ab17ca4f1637108170a533a9e7a3ae603bd386f96f4a7115bf28
1+a6658304228f3f3637147ff877110d1fb4cb441db24a871da0543c9b5d997966
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.fsproj.CoreCompileInputs.cache+1−1
@@ -1 +1 @@
1−69642e04cc9acf8f904dc028dba9bb2fb594ca3a355cba1325fba75dad676d69
1+0182c29e4e78ed8ffdff7ee936a192ffc3599905f475905b18569649ce05748a
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.fsproj.FileListAbsolute.txt+23
@@ -334,3 +334,26 @@
334334/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.genruntimeconfig.cache
335335/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/ref/Skrepka.dll
336336/Users/iko/Developer/skrepka/client/bin/Debug/net10.0-ios/iossimulator-arm64/LiteDB.dll
337+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Fabulous.stamp
338+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Fabulous.MauiControls.stamp
339+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/FSharp.Core.stamp
340+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/LiteDB.stamp
341+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Configuration.Abstractions.stamp
342+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Configuration.stamp
343+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.DependencyInjection.Abstractions.stamp
344+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.DependencyInjection.stamp
345+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Diagnostics.Abstractions.stamp
346+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.FileProviders.Abstractions.stamp
347+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Hosting.Abstractions.stamp
348+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Logging.Abstractions.stamp
349+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Logging.stamp
350+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Options.stamp
351+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Primitives.stamp
352+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.Compatibility.stamp
353+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.stamp
354+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.Xaml.stamp
355+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.stamp
356+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Essentials.stamp
357+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Graphics.stamp
358+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Sodium.Core.stamp
359+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/unpack/Microsoft.Maui.Essentials/BundleResource/xml/microsoft_maui_essentials_fileprovider_file_paths.xml
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb

Binary file not shown.

client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.sourcelink.json+1−1
@@ -1 +1 @@
1−{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/5c34fab63923cdb1b780a7e5a5ce5fae72adf78e/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/5a91ff165d267f1b48cd27804ddc67fbfbb1a12b/*"}}
client/obj/Debug/net10.0-ios/iossimulator-arm64/ref/Skrepka.dll

Binary file not shown.

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

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+f4f8a00e43e943aaf1ab17647f6d1e4f055c7d3a")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+5a91ff165d267f1b48cd27804ddc67fbfbb1a12b")>]
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−d1e3dd8bf6923e3c33b8e7c46138a01be4bdfd2c1de6dfe8e4da64444b301b79
1+bab902372413363aadc7dd6fd03839ec75cc7bfbd2431aa615e69476a889187f
client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.fsproj.CoreCompileInputs.cache+1−1
@@ -1 +1 @@
1−6202acc233295b7d078b1e7cc174d976ebb69fcd5075adc57d9dd3761c9e9f75
1+e72de62331c5600bbb3125cfe2f57f32602258ae53f7d082ac6aaf123c600e47
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/c0cce11a1754909d3aa01c81e84df143a9bcd275/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/5a91ff165d267f1b48cd27804ddc67fbfbb1a12b/*"}}
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+39−35
@@ -48,9 +48,7 @@ federationEnabled = federationSecret != ""
4848-- Types
4949-- ============================================
5050
51−type PollEventType = Text where \s -> s == "message"
52−
53−type PollEvent = {id: MessageIdHex, eventType: PollEventType, payload: {encryptedBlob: BlobHex, fromKey: PubkeyHex, timestamp: Timestamp, toKey: PubkeyHex}}
51+type PollEvent = {id: MessageIdHex, eventType: Text, payload: {encryptedBlob: BlobHex, fromKey: PubkeyHex, timestamp: Timestamp, toKey: PubkeyHex}}
5452
5553type GossipEventType = Text where \s -> s == "online" || s == "offline"
5654
@@ -58,26 +56,29 @@ type GossipEvent = {eventType: GossipEventType, pubkey: PubkeyHex, server: Serve
5856
5957type ValidGossipEntry = {eventType: GossipEventType, pubkey: PubkeyHex, server: ServerName, expiresAt: Timestamp, timestamp: Timestamp, ttl: Int<S>}
6058
61−type MessageStatus = Text where \s -> s == "unauthorized" || s == "rejected" || s == "delivered" || s == "federated" || s == "queued"
59+isDeliveryStatus = \s -> s == "delivered" || s == "federated" || s == "queued"
60+type MessageStatus = Text where \s -> s == "unauthorized" || s == "rejected" || isDeliveryStatus s
6261
6362type PubkeyHex = Text where \s -> isValidHex s && not (tooLong s maxPubkeyLen) && s == normalizeHex s
6463type BlobHex = Text where \s -> isValidHex s && not (tooLong s maxBlobLen) && s == normalizeHex s
6564type SignatureHex = Text where \s -> isValidHex s && not (tooLong s maxSignatureLen) && s == normalizeHex s
6665type MessageIdHex = Text where \s -> isValidHex s && not (tooLong s maxMessageIdLen) && s == normalizeHex s
67−type ServerName = Text where \s -> not (tooLong s maxServerNameLen) && s == normalizeServerName s && not (isBadServerName s)
68−type OptionalServerName = Text where \s -> s == "" || (not (tooLong s maxServerNameLen) && s == normalizeServerName s && not (isBadServerName s))
66+isValidServerName = \s -> not (tooLong s maxServerNameLen) && s == normalizeServerName s && not (isBadServerName s)
67+type ServerName = Text where isValidServerName
68+type OptionalServerName = Text where \s -> s == "" || isValidServerName s
69+type AnyServerName = Text where \s -> not (tooLong s maxServerNameLen) && s == normalizeServerName s
6970
7071type Timestamp = Int<Ms>
7172
7273type Challenge = {pubkey: PubkeyHex, challenge: Text, expiresAt: Timestamp}
7374type Session = {token: Text, pubkey: PubkeyHex, expiresAt: Timestamp}
7475type Message = {messageId: MessageIdHex, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, originServer: OptionalServerName, receivedAt: Timestamp}
75−type PresenceEntry = {pubkey: PubkeyHex, server: Text, expiresAt: Timestamp, timestamp: Timestamp}
76+type PresenceEntry = {pubkey: PubkeyHex, server: AnyServerName, expiresAt: Timestamp, timestamp: Timestamp}
7677type ForwardEntry = {messageId: MessageIdHex, toServer: ServerName, createdAt: Timestamp, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp}
7778type AckedEntry = {messageId: MessageIdHex, toKey: PubkeyHex, ackedAt: Timestamp, originServer: ServerName}
7879type AuthResult = Authorized {pubkey: PubkeyHex} | Unauthorized {}
7980type PruneResult = {expiredLocal: [PresenceEntry], peerServers: [ServerName]}
80−type DeliveryStatus = Text where \s -> s == "delivered" || s == "federated" || s == "queued"
81+type DeliveryStatus = Text where isDeliveryStatus
8182type SendResult = SendUnauthorized {} | SendRejected {} | SendDone {status: DeliveryStatus, remoteServers: [ServerName], fromKey: PubkeyHex}
8283
8384
@@ -265,14 +266,16 @@ normalizeServerName = \s -> toLower (stripTrailingDot s)
265266
266267serverHostN = normalizeServerName serverHost
267268
268−containsSuffix = \suffix suffixLen s ->
269+containsSuffixN = \suffix suffixLen s ->
269270 if s == "" then False {}
270271 else if take suffixLen s == suffix && (drop suffixLen s == "" || take 1 (drop suffixLen s) == ":") then True {}
271− else containsSuffix suffix suffixLen (drop 1 s)
272+ else containsSuffixN suffix suffixLen (drop 1 s)
273+
274+containsSuffix = \suffix s -> containsSuffixN suffix (count (chars suffix)) s
272275
273−containsLocalhostSuffix = \s -> containsSuffix ".localhost" 10 s
274−containsLocalSuffix = \s -> containsSuffix ".local" 6 s
275−containsInternalSuffix = \s -> containsSuffix ".internal" 9 s
276+containsLocalhostSuffix = \s -> containsSuffix ".localhost" s
277+containsLocalSuffix = \s -> containsSuffix ".local" s
278+containsInternalSuffix = \s -> containsSuffix ".internal" s
276279
277280is172Private = \sn ->
278281 take 4 sn == "172."
@@ -463,6 +466,22 @@ removeOffline = \presenceSet offlineOnlyKeys trustedOffline t ->
463466 where samePubkeyServer p k && any trustedOffline (\o -> samePubkeyServer o k && (if o.timestamp > t then t else o.timestamp) > p.timestamp)
464467 yield p)
465468
469+capGossipPresence = \afterOffline appliedOnline trustedDeduped presence offlineOnlyKeys trustedOffline t ->
470+ if count afterOffline <= maxPresenceEntries
471+ then {finalPresence: afterOffline, appliedOnline: appliedOnline, withinCapacity: True {}}
472+ else
473+ let updatesOnly = do e <- trustedDeduped; where any presence (\p -> samePubkeyServer p e); yield e in
474+ let updatesOnlyMerged = mergeByPubkeyServer presence updatesOnly in
475+ let updatesOnlyAfterOffline = removeOffline updatesOnlyMerged offlineOnlyKeys trustedOffline t in
476+ let reclaimSlots = maxPresenceEntries - count updatesOnlyAfterOffline in
477+ let newEntries = do e <- trustedDeduped; where none presence (\p -> samePubkeyServer p e); yield e in
478+ let reclaimedEntries = takeN newEntries reclaimSlots in
479+ {finalPresence: union updatesOnlyAfterOffline reclaimedEntries,
480+ appliedOnline: union
481+ (do e <- updatesOnly; where none presence (\p -> samePubkeyServer p e && p.expiresAt >= e.expiresAt); yield {pubkey: e.pubkey, server: e.server})
482+ (do e <- reclaimedEntries; yield {pubkey: e.pubkey, server: e.server}),
483+ withinCapacity: False {}}
484+
466485countMessagesFor = \messages toKey t ->
467486 count (do
468487 m <- messages
@@ -617,7 +636,7 @@ retryEvictedAcks = \evictedEntries pendingAcks ->
617636 a <- evictedEntries
618637 where none pendingAcks (\a2 -> a2.messageId == a.messageId && a2.toKey == a.toKey)
619638 yield a
620− in when (count evictedToRetry > 0) (do
639+ in do
621640 let evictedOriginServers = collectPeerServers evictedToRetry (\a -> a.originServer)
622641 sendToEach evictedOriginServers (\server -> do
623642 fetchResult <- fetch (federationScheme ++ server) (FederationAck {
@@ -637,7 +656,7 @@ retryEvictedAcks = \evictedEntries pendingAcks ->
637656 yield excess
638657 when (dropped > 0)
639658 (println ("ackMessages: ackedIds at capacity (" ++ show maxAckedIds ++ "), " ++ show dropped ++ " ack entries permanently lost for " ++ server))
640− _ -> yield {}))
659+ _ -> yield {})
641660
642661pruneAndRetry = \t -> do
643662 result <- atomic do
@@ -649,11 +668,9 @@ pruneAndRetry = \t -> do
649668 yield {expiredLocal: pr.expiredLocal, peerServers: pr.peerServers, pendingForwards: forwards, pendingAcks: pendingAcks}
650669 broadcastOfflineGossip t result.peerServers result.expiredLocal
651670 when federationEnabled (do
652− when (count result.pendingForwards > 0)
653− (retryPendingForwards result.pendingForwards)
654− when (count result.pendingAcks > 0) (do
655− let ackOriginServers = collectPeerServers result.pendingAcks (\a -> a.originServer)
656− sendAcksAndClean result.pendingAcks ackOriginServers))
671+ retryPendingForwards result.pendingForwards
672+ let ackOriginServers = collectPeerServers result.pendingAcks (\a -> a.originServer)
673+ sendAcksAndClean result.pendingAcks ackOriginServers)
657674
658675pollOnce = \token cursor -> do
659676 t <- now
@@ -920,23 +937,10 @@ serve = \req -> case req of
920937 where any presence (\p -> samePubkeyServer p k && any trustedOffline (\o -> samePubkeyServer o k && (if o.timestamp > t then t else o.timestamp) > p.timestamp))
921938 yield {pubkey: k.pubkey, server: k.server}
922939 let afterOffline = removeOffline merged offlineOnlyKeys trustedOffline t
923− let withinCapacity = count afterOffline <= maxPresenceEntries
924− let capResult = if withinCapacity
925− then {finalPresence: afterOffline, appliedOnline: appliedOnline}
926− else
927− let updatesOnly = do e <- trustedDeduped; where any presence (\p -> samePubkeyServer p e); yield e in
928− let updatesOnlyMerged = mergeByPubkeyServer presence updatesOnly in
929− let updatesOnlyAfterOffline = removeOffline updatesOnlyMerged offlineOnlyKeys trustedOffline t in
930− let reclaimSlots = maxPresenceEntries - count updatesOnlyAfterOffline in
931− let newEntries = do e <- trustedDeduped; where none presence (\p -> samePubkeyServer p e); yield e in
932− let reclaimedEntries = takeN newEntries reclaimSlots in
933− {finalPresence: union updatesOnlyAfterOffline reclaimedEntries,
934− appliedOnline: union
935− (do e <- updatesOnly; where none presence (\p -> samePubkeyServer p e && p.expiresAt >= e.expiresAt); yield {pubkey: e.pubkey, server: e.server})
936− (do e <- reclaimedEntries; yield {pubkey: e.pubkey, server: e.server})}
940+ let capResult = capGossipPresence afterOffline appliedOnline trustedDeduped presence offlineOnlyKeys trustedOffline t
937941 set *presence = capResult.finalPresence
938942 let gossipPeerServers = collectPeerServers capResult.finalPresence (\p -> p.server)
939− yield {appliedKeys: union capResult.appliedOnline appliedOffline, pr: pr, gossipPeerServers: gossipPeerServers, withinCapacity: withinCapacity}
943+ yield {appliedKeys: union capResult.appliedOnline appliedOffline, pr: pr, gossipPeerServers: gossipPeerServers, withinCapacity: capResult.withinCapacity}
940944 let effectiveEvents = do
941945 e <- union filteredOnline filteredOffline
942946 where any gossipResult.appliedKeys (\a -> samePubkeyServer a e)