on master help sign in
Simplify server.knot: remove single-variant PollEventType, optimize fold-based operations, clarify JSON parsing
commita5a5e7c5bac92a6f1d566920ab29663bfc3aad5f authoriko <mail@iko.soy> dateFri 10 Apr 2026, 20:26 (6 months ago) parente3148dd
27 files changed+140−121
client/App.fs+81−78 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.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.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+24−31
client/App.fs+81−78
@@ -40,8 +40,8 @@ module App =
4040 type Page =
4141 | Setup
4242 | Conversations
43− | Chat of pubkey: string
44− | AddContact
43+ | Chat of pubkey: string * compose: string
44+ | AddContact of pubkey: string * nickname: string
4545 | Settings
4646
4747 type Session = { Url: string; Token: string; Identity: Identity }
@@ -55,6 +55,14 @@ module App =
5555 | NoIdentity
5656 | Identified of identity: Identity * conn: ConnStatus
5757
58+ type IncomingMessage = { SenderPubkey: string; Message: ChatMessage }
59+
60+ type PollStatus =
61+ | Idle
62+ | Polling
63+ | Received of events: int * messages: int * errors: int * firstError: string option
64+ | PollError of string
65+
5866 // ── Model ──
5967
6068 type Model =
@@ -62,13 +70,10 @@ module App =
6270 Auth: AuthState
6371 ServerUrl: string
6472 Contacts: Contact list
65− PollStatus: string
73+ PollStatus: PollStatus
6674 Messages: Map<string, ChatMessage list>
6775 Error: string option
68− PollCursor: int64
69− Compose: string
70− ContactPubkey: string
71− ContactNickname: string }
76+ PollCursor: int64 }
7277
7378 // ── Msg ──
7479
@@ -92,7 +97,7 @@ module App =
9297 | DoSend
9398 | SendFailed of string
9499 | DoSaveContact
95− | PollResult of messages: (string * ChatMessage) list * status: string * cursor: int64
100+ | PollResult of messages: IncomingMessage list * status: PollStatus * cursor: int64
96101 | CopyPubKey
97102 | DismissError
98103 | StateLoaded of StateLoadResult
@@ -111,12 +116,6 @@ module App =
111116
112117 // ── Helpers ──
113118
114− let private nowStr () =
115− DateTimeOffset.UtcNow.ToString("HH:mm:ss")
116−
117− let private truncate maxLen (s: string) =
118− if s.Length > maxLen then s.[..maxLen - 1] + "..." else s
119−
120119 let private hexToOb (hex: string) =
121120 Crypto.fromHex hex |> Phonemic.toOb
122121
@@ -178,29 +177,35 @@ module App =
178177 { Page = Setup
179178 Auth = NoIdentity
180179 ServerUrl = "http://localhost:8080"
181− PollStatus = ""
180+ PollStatus = Idle
182181 Contacts = []
183182 Messages = Map.empty
184183 Error = None
185− PollCursor = 0L
186− Compose = ""
187− ContactPubkey = ""
188− ContactNickname = "" },
184+ PollCursor = 0L },
189185 [ CmdLoadState ]
190186
191187 // ── Update ──
192188
193189 let update msg model =
194190 match msg with
195− | SetPage(Chat pk) ->
196− { model with Page = Chat pk; Compose = "" }, []
197− | SetPage AddContact ->
198− { model with Page = AddContact; ContactPubkey = ""; ContactNickname = "" }, []
191+ | SetPage(Chat(pk, _)) ->
192+ { model with Page = Chat(pk, "") }, []
193+ | SetPage(AddContact _) ->
194+ { model with Page = AddContact("", "") }, []
199195 | SetPage page -> { model with Page = page }, []
200196
201− | SetCompose text -> { model with Compose = text }, []
202− | SetContactPubkey pk -> { model with ContactPubkey = pk }, []
203− | SetContactNickname nn -> { model with ContactNickname = nn }, []
197+ | SetCompose text ->
198+ match model.Page with
199+ | Chat(pk, _) -> { model with Page = Chat(pk, text) }, []
200+ | _ -> model, []
201+ | SetContactPubkey pk ->
202+ match model.Page with
203+ | AddContact(_, nn) -> { model with Page = AddContact(pk, nn) }, []
204+ | _ -> model, []
205+ | SetContactNickname nn ->
206+ match model.Page with
207+ | AddContact(pk, _) -> { model with Page = AddContact(pk, nn) }, []
208+ | _ -> model, []
204209
205210 | GenIdentity -> model, [ CmdGenIdentity ]
206211
@@ -230,21 +235,20 @@ module App =
230235
231236 | DoSend ->
232237 match model.Page, trySession model with
233− | Chat pk, Some session when model.Compose <> "" ->
234− let text = model.Compose
238+ | Chat(pk, compose), Some session when compose <> "" ->
235239 let id = Guid.NewGuid().ToString()
236240 let outMsg =
237241 { Id = id
238− Body = text
242+ Body = compose
239243 Timestamp = DateTimeOffset.UtcNow
240244 IsOutgoing = true }
241245
242246 let model' =
243247 { model with
244− Compose = ""
248+ Page = Chat(pk, "")
245249 Messages = model.Messages |> appendMessage pk outMsg }
246250
247− model', [ CmdSend(session, pk, text, id); saveCmdMsg model' ]
251+ model', [ CmdSend(session, pk, compose, id); saveCmdMsg model' ]
248252 | _ -> model, []
249253
250254 | SendFailed err -> { model with Error = Some err }, []
@@ -253,10 +257,10 @@ module App =
253257 let model' =
254258 incoming
255259 |> List.fold
256− (fun (m: Model) (fromPubkey, chatMsg) ->
260+ (fun (m: Model) msg ->
257261 { m with
258− Contacts = m.Contacts |> addContactIfNew { Pubkey = fromPubkey; Nickname = truncKey fromPubkey }
259− Messages = m.Messages |> appendMessage fromPubkey chatMsg })
262+ Contacts = m.Contacts |> addContactIfNew { Pubkey = msg.SenderPubkey; Nickname = truncKey msg.SenderPubkey }
263+ Messages = m.Messages |> appendMessage msg.SenderPubkey msg.Message })
260264 { model with PollStatus = status; PollCursor = newCursor }
261265
262266 model',
@@ -266,9 +270,8 @@ module App =
266270 if not incoming.IsEmpty then saveCmdMsg model' ]
267271
268272 | DoSaveContact ->
269− let pk, nn = model.ContactPubkey, model.ContactNickname
270273 match model.Page with
271− | AddContact when pk <> "" && nn <> "" ->
274+ | AddContact(pk, nn) when pk <> "" && nn <> "" ->
272275 match tryParsePubkey pk with
273276 | Some bytes ->
274277 let contact: Contact = { Pubkey = Crypto.toHex bytes; Nickname = nn }
@@ -323,11 +326,12 @@ module App =
323326
324327 match envelope.Type with
325328 | Text ->
326− Ok(senderHex,
327− { Id = envelope.Id
328− Body = envelope.Body
329− Timestamp = DateTimeOffset.FromUnixTimeSeconds(payload.Timestamp)
330− IsOutgoing = false })
329+ Ok { SenderPubkey = senderHex
330+ Message =
331+ { Id = envelope.Id
332+ Body = envelope.Body
333+ Timestamp = DateTimeOffset.FromUnixTimeSeconds(payload.Timestamp)
334+ IsOutgoing = false } }
331335 | UnknownEnvelope t -> Error $"unknown envelope type: {t}"
332336 | None -> Error "decrypt failed"
333337 with ex ->
@@ -388,20 +392,14 @@ module App =
388392 if not ackIds.IsEmpty then
389393 do! ackMessages session.Url session.Token ackIds
390394
391− let now = nowStr ()
392−
393− let status =
394− $"[{now}] evts:{response.Events.Length} msgs:{messages.Length} errs:{errors.Length}"
395− + (match errors with err :: _ -> $" [{err}]" | [] -> "")
396−
395+ let status = Received(response.Events.Length, messages.Length, errors.Length, List.tryHead errors)
397396 return PollResult(messages, status, response.Cursor)
398397 with
399398 | :? TimeoutException ->
400− return PollResult([], "polling...", cursor)
399+ return PollResult([], Polling, cursor)
401400 | ex ->
402− let now = nowStr ()
403401 do! Async.Sleep 3000
404− return PollResult([], $"[{now}] poll error: {ex.Message}", cursor)
402+ return PollResult([], PollError ex.Message, cursor)
405403 })
406404
407405 | CmdCopyToClipboard text ->
@@ -428,6 +426,14 @@ module App =
428426
429427 // ── View ──
430428
429+ let private formatPollStatus = function
430+ | Idle -> ""
431+ | Polling -> "polling..."
432+ | Received(evts, msgs, errs, firstErr) ->
433+ $"evts:{evts} msgs:{msgs} errs:{errs}"
434+ + (match firstErr with Some e -> $" [{e}]" | None -> "")
435+ | PollError msg -> $"poll error: {msg}"
436+
431437 let private viewSetup () =
432438 ContentPage(
433439 (VStack(spacing = 24.) {
@@ -495,14 +501,6 @@ module App =
495501 | Identified(_, Connecting) -> "CONNECTING"
496502 | Identified(_, Online _) -> "ONLINE"
497503
498− let contactNames =
499− model.Contacts
500− |> List.map _.Nickname
501− |> String.concat ", "
502−
503− let totalMsgs =
504− model.Messages |> Map.values |> Seq.sumBy List.length
505−
506504 ContentPage(
507505 ScrollView(
508506 (VStack(spacing = 8.) {
@@ -510,22 +508,19 @@ module App =
510508 Label("Conversations")
511509 .font(size = 24.)
512510
513− Button("+", SetPage AddContact)
511+ Button("+", SetPage(AddContact("", "")))
514512 Button("Settings", SetPage Settings)
515513 }
516514
517− Label($"conn:{connStr} contacts:{model.Contacts.Length} msgs:{totalMsgs}")
518− .font(size = 10.)
519− .textColor(Colors.DimGray)
520−
521− Label($"poll: {model.PollStatus}")
515+ Label($"{connStr} | {formatPollStatus model.PollStatus}")
522516 .font(size = 10.)
523517 .textColor(Colors.DimGray)
524518
525− if contactNames <> "" then
526− Label($"names: {contactNames}")
527− .font(size = 10.)
528− .textColor(Colors.DimGray)
519+ match model.Error with
520+ | Some err ->
521+ Label(err).textColor(Colors.Red).font(size = 12.)
522+ Button("Dismiss", DismissError)
523+ | None -> ()
529524
530525 if model.Contacts.IsEmpty then
531526 Label("No contacts yet. Tap + to add one.")
@@ -536,16 +531,16 @@ module App =
536531 let preview =
537532 messagesFor c.Pubkey model.Messages
538533 |> List.tryLast
539− |> Option.map (fun m -> truncate 40 m.Body)
534+ |> Option.map (fun m -> if m.Body.Length > 40 then m.Body.[..39] + "..." else m.Body)
540535 |> Option.defaultValue ""
541536
542− Button($"{c.Nickname}\n{preview}", SetPage(Chat c.Pubkey))
537+ Button($"{c.Nickname}\n{preview}", SetPage(Chat(c.Pubkey, "")))
543538 })
544539 .padding(20.)
545540 )
546541 )
547542
548− let private viewChat model pk =
543+ let private viewChat model pk compose =
549544 let name = contactName model.Contacts pk
550545 let msgs = messagesFor pk model.Messages
551546
@@ -563,7 +558,9 @@ module App =
563558 | Some err ->
564559 Label(err)
565560 .textColor(Colors.Red)
566− .font(size = 11.)
561+ .font(size = 12.)
562+
563+ Button("Dismiss", DismissError)
567564 | None -> ()
568565
569566 if msgs.IsEmpty then
@@ -577,7 +574,7 @@ module App =
577574 .font(size = 14.)
578575
579576 HStack(spacing = 8.) {
580− Entry(model.Compose, SetCompose)
577+ Entry(compose, SetCompose)
581578 .placeholder("Message...")
582579
583580 Button("Send", DoSend)
@@ -586,7 +583,7 @@ module App =
586583 .padding(12.)
587584 )
588585
589− let private viewAddContact model =
586+ let private viewAddContact model pubkey nickname =
590587 ContentPage(
591588 (VStack(spacing = 16.) {
592589 HStack(spacing = 8.) {
@@ -596,12 +593,18 @@ module App =
596593 .font(size = 20.)
597594 }
598595
596+ match model.Error with
597+ | Some err ->
598+ Label(err).textColor(Colors.Red).font(size = 12.)
599+ Button("Dismiss", DismissError)
600+ | None -> ()
601+
599602 Label("Public Key:")
600− Entry(model.ContactPubkey, SetContactPubkey)
603+ Entry(pubkey, SetContactPubkey)
601604 .placeholder("sampel-palnet-...")
602605
603606 Label("Nickname:")
604− Entry(model.ContactNickname, SetContactNickname)
607+ Entry(nickname, SetContactNickname)
605608
606609 Button("Save Contact", DoSaveContact)
607610 .centerHorizontal()
@@ -615,8 +618,8 @@ module App =
615618 | Setup -> viewSetup ()
616619 | Settings -> viewSettings model
617620 | Conversations -> viewConversations model
618− | Chat pk -> viewChat model pk
619− | AddContact -> viewAddContact model
621+ | Chat(pk, compose) -> viewChat model pk compose
622+ | AddContact(pubkey, nickname) -> viewAddContact model pubkey nickname
620623
621624 Application() { Window(page) }
622625
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− 17Ja2NZg0tFd466CxnE4FAYcaCU=
169+ XZqIgqY1iuk+FHTMZbQg6H06BPc=
170170 </data>
171171 <key>Skrepka.pdb</key>
172172 <data>
173− HDd9myvi38aIkE3tHWkWvybpITE=
173+ jGsllzUykGPLD645ovFAhjzyj0U=
174174 </data>
175175 <key>Sodium.Core.dll</key>
176176 <data>
@@ -1393,14 +1393,14 @@
13931393 <dict>
13941394 <key>hash2</key>
13951395 <data>
1396− sg0ozQt55G8nT4JyqSmTfQ0OiW2Hh79ThNsHjl9JMrg=
1396+ bxgl7DOK4AVvfqG7HqK1vCim2a91AGyHoW/pw+rbnjQ=
13971397 </data>
13981398 </dict>
13991399 <key>Skrepka.pdb</key>
14001400 <dict>
14011401 <key>hash2</key>
14021402 <data>
1403− IgABOviFh2f026awA/Fpe6il6FzUtRP9ssoE1G6a39E=
1403+ 3Fifs0NPR+poxKmLRf5cxvJCm1fk9qFg9gOZSl1VkC0=
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− 3BE8YIxbdGoJrALNRoC50MYvlg4zsLOk7ap2LT2m948=
254+ sJGkuR5gxMR4CxhoKVU1LJTdCM3P3wmaAaPscSiCdwk=
255255 </data>
256256 </dict>
257257 <key>MonoBundle/Skrepka.pdb</key>
258258 <dict>
259259 <key>hash2</key>
260260 <data>
261− 6rH6M4XCjLG1C6imAiKYY4h+PeM762EdbZ4CI+q0O0g=
261+ ANJi//INYTZtSEEDMIzRTGBB9feNGEHg+ZFulqWLaDc=
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+6b1d74aecd0ed0fb71c97a95acff19b5fb9bf149")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+e3148dde3e9c36905f2c3f85861acc4776bc1da2")>]
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−f888d360dad95a9d0eb67556097c9976f7b2a97dc5d086fc94022bbe43b965a8
1+6df29f3db280d4b4f3321357f916fe7e6dceca5ce10d790d3c17f749db0009ea
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll

Binary file not shown.

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/6b1d74aecd0ed0fb71c97a95acff19b5fb9bf149/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/e3148dde3e9c36905f2c3f85861acc4776bc1da2/*"}}
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+e6d52540f7a36ceb966de06191cfd7c3d223c800")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+e3148dde3e9c36905f2c3f85861acc4776bc1da2")>]
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−35309a0fe3ed35bcd80b2e755c52f81fc42b7e6e3c07f1b3a7cf0e54f7919e32
1+edd6f79032ad5ae904eb2e4839e47e18b6506d82914ddfc23a23c0b225656098
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/e6d52540f7a36ceb966de06191cfd7c3d223c800/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/e3148dde3e9c36905f2c3f85861acc4776bc1da2/*"}}
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+24−31
@@ -48,15 +48,7 @@ federationEnabled = federationSecret != ""
4848-- Types
4949-- ============================================
5050
51−type PollEventType = PollMessage {}
52−
53−impl ToJSON PollEventType where
54− toJson _ = toJson "message"
55−
56−impl FromJSON PollEventType where
57− parseJson _ = PollMessage {}
58−
59−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}}
6052
6153type GossipEventType = GossipOnline {} | GossipOffline {}
6254
@@ -68,7 +60,9 @@ impl ToJSON GossipEventType where
6860impl FromJSON GossipEventType where
6961 parseJson t =
7062 let s : Text = parseJson t in
71− if s == "online" then GossipOnline {} else GossipOffline {}
63+ if s == "online" then GossipOnline {}
64+ else if s == "offline" then GossipOffline {}
65+ else GossipOffline {}
7266
7367isGossipOnline = \et -> case et of GossipOnline {} -> True {}; _ -> False {}
7468
@@ -93,6 +87,7 @@ impl FromJSON MessageStatus where
9387 else if s == "rejected" then MsRejected {}
9488 else if s == "delivered" then MsDelivered {}
9589 else if s == "federated" then MsFederated {}
90+ else if s == "queued" then MsQueued {}
9691 else MsQueued {}
9792
9893type PubkeyHex = Text where \s -> isValidHex s && not (tooLong s maxPubkeyLen) && s == normalizeHex s
@@ -488,20 +483,20 @@ mergeByKey = \sameKey getFreshness initial items ->
488483
489484mergeByPubkeyServer = mergeByKey samePubkeyServer (\x -> x.expiresAt)
490485
491−removeOffline = \presenceSet offlineOnlyKeys trustedOffline t ->
486+removeOffline = \presenceSet trustedOffline t ->
492487 diff presenceSet (do
493488 p <- presenceSet
494− k <- offlineOnlyKeys
495− where samePubkeyServer p k && any trustedOffline (\o -> samePubkeyServer o k && (if o.timestamp > t then t else o.timestamp) > p.timestamp)
489+ o <- trustedOffline
490+ where samePubkeyServer p o && (if o.timestamp > t then t else o.timestamp) > p.timestamp
496491 yield p)
497492
498−capGossipPresence = \afterOffline appliedOnline trustedDeduped presence offlineOnlyKeys trustedOffline t ->
493+capGossipPresence = \afterOffline appliedOnline trustedDeduped presence trustedOffline t ->
499494 if count afterOffline <= maxPresenceEntries
500495 then {finalPresence: afterOffline, appliedOnline: appliedOnline, withinCapacity: True {}}
501496 else
502497 let updatesOnly = do e <- trustedDeduped; where any presence (\p -> samePubkeyServer p e); yield e in
503498 let updatesOnlyMerged = mergeByPubkeyServer presence updatesOnly in
504− let updatesOnlyAfterOffline = removeOffline updatesOnlyMerged offlineOnlyKeys trustedOffline t in
499+ let updatesOnlyAfterOffline = removeOffline updatesOnlyMerged trustedOffline t in
505500 let reclaimSlots = maxPresenceEntries - count updatesOnlyAfterOffline in
506501 let newEntries = do e <- trustedDeduped; where none presence (\p -> samePubkeyServer p e); yield e in
507502 let reclaimedEntries = takeN newEntries reclaimSlots in
@@ -512,10 +507,7 @@ capGossipPresence = \afterOffline appliedOnline trustedDeduped presence offlineO
512507 withinCapacity: False {}}
513508
514509countMessagesFor = \messages toKey t ->
515− count (do
516− m <- messages
517− where m.toKey == toKey && m.receivedAt >= t - messageMaxAge
518− yield m)
510+ fold (\n m -> if m.toKey == toKey && m.receivedAt >= t - messageMaxAge then n + 1 else n) 0 messages
519511
520512
521513validGossipEvents : [GossipEvent] -> Timestamp -> [ValidGossipEntry]
@@ -577,13 +569,18 @@ maybePrune = \t -> do
577569 set *lastPrunedAt = t
578570 pruneExpired t
579571
572+splitAtN = \items n ->
573+ fold (\acc e ->
574+ if acc.n < n then {n: acc.n + 1, batch: union acc.batch [e], rest: acc.rest}
575+ else {n: acc.n, batch: acc.batch, rest: union acc.rest [e]})
576+ {n: 0, batch: [], rest: []} items
577+
580578forEachBatch = \items batchSize process ->
581579 if count items == 0 then yield {}
582580 else do
583− let batch = takeN items batchSize
584− let rest = diff items batch
585− process batch
586− forEachBatch rest batchSize process
581+ let split = splitAtN items batchSize
582+ process split.batch
583+ forEachBatch split.rest batchSize process
587584
588585sendBatchedGossipToServer : ServerName -> [GossipEvent] -> Int -> IO {network} {}
589586sendBatchedGossipToServer = \server allEvents hops ->
@@ -721,7 +718,7 @@ pollOnce = \token cursor -> do
721718 let limitedMsgs = boundedSmallest matchingMsgs maxPollEvents (\m -> m.receivedAt)
722719 let evts = do
723720 m <- limitedMsgs
724− yield {id: m.messageId, eventType: PollMessage {},
721+ yield {id: m.messageId, eventType: "message",
725722 payload: {toKey: m.toKey, fromKey: m.fromKey,
726723 encryptedBlob: m.encryptedBlob,
727724 timestamp: m.timestamp}}
@@ -931,7 +928,7 @@ serve = \req -> case req of
931928 else do
932929 let allValidEntries = validGossipEvents events t
933930 let onlineEntries = do e <- allValidEntries; where isGossipOnline e.eventType; yield e
934− let offlineEntries = diff allValidEntries onlineEntries
931+ let offlineEntries = do e <- allValidEntries; where not (isGossipOnline e.eventType); yield e
935932 let trustedOffline = do e <- offlineEntries; where e.server == fromServer; yield e
936933 let filteredOnline = do
937934 e <- onlineEntries
@@ -954,13 +951,9 @@ serve = \req -> case req of
954951 where none presence (\p -> samePubkeyServer p e && p.expiresAt >= e.expiresAt)
955952 yield {pubkey: e.pubkey, server: e.server}
956953 let merged = mergeByPubkeyServer presence trustedDeduped
957− let offlineOnlyKeys = do
958− e <- allValidEntries
959− where e.server == fromServer && none trustedDeduped (\d -> samePubkeyServer d e)
960− yield e
961− let afterOffline = removeOffline merged offlineOnlyKeys trustedOffline t
954+ let afterOffline = removeOffline merged trustedOffline t
962955 let appliedOffline = do p <- diff merged afterOffline; yield {pubkey: p.pubkey, server: p.server}
963− let capResult = capGossipPresence afterOffline appliedOnline trustedDeduped presence offlineOnlyKeys trustedOffline t
956+ let capResult = capGossipPresence afterOffline appliedOnline trustedDeduped presence trustedOffline t
964957 set *presence = capResult.finalPresence
965958 let gossipPeerServers = collectPeerServers capResult.finalPresence (\p -> p.server)
966959 yield {appliedKeys: union capResult.appliedOnline appliedOffline, pr: pr, gossipPeerServers: gossipPeerServers, withinCapacity: capResult.withinCapacity}