on master help sign in
Simplify server.knot: remove single-variant PollEventType, optimize fold-based operations, clarify JSON parsing
a5a5e7ciko
client/App.fs+81−78
@@ -40,8 +40,8 @@ module App =
40 type Page =
40 type Page =
41 | Setup
41 | Setup
42 | Conversations
42 | Conversations
43− | Chat of pubkey: string
43+ | Chat of pubkey: string * compose: string
44− | AddContact
44+ | AddContact of pubkey: string * nickname: string
45 | Settings
45 | Settings
46
46
47 type Session = { Url: string; Token: string; Identity: Identity }
47 type Session = { Url: string; Token: string; Identity: Identity }
@@ -55,6 +55,14 @@ module App =
55 | NoIdentity
55 | NoIdentity
56 | Identified of identity: Identity * conn: ConnStatus
56 | Identified of identity: Identity * conn: ConnStatus
57
57
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+
58 // ── Model ──
66 // ── Model ──
59
67
60 type Model =
68 type Model =
@@ -62,13 +70,10 @@ module App =
62 Auth: AuthState
70 Auth: AuthState
63 ServerUrl: string
71 ServerUrl: string
64 Contacts: Contact list
72 Contacts: Contact list
65− PollStatus: string
73+ PollStatus: PollStatus
66 Messages: Map<string, ChatMessage list>
74 Messages: Map<string, ChatMessage list>
67 Error: string option
75 Error: string option
68− PollCursor: int64
76+ PollCursor: int64 }
69− Compose: string
70− ContactPubkey: string
71− ContactNickname: string }
72
77
73 // ── Msg ──
78 // ── Msg ──
74
79
@@ -92,7 +97,7 @@ module App =
92 | DoSend
97 | DoSend
93 | SendFailed of string
98 | SendFailed of string
94 | DoSaveContact
99 | DoSaveContact
95− | PollResult of messages: (string * ChatMessage) list * status: string * cursor: int64
100+ | PollResult of messages: IncomingMessage list * status: PollStatus * cursor: int64
96 | CopyPubKey
101 | CopyPubKey
97 | DismissError
102 | DismissError
98 | StateLoaded of StateLoadResult
103 | StateLoaded of StateLoadResult
@@ -111,12 +116,6 @@ module App =
111
116
112 // ── Helpers ──
117 // ── Helpers ──
113
118
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−
120 let private hexToOb (hex: string) =
119 let private hexToOb (hex: string) =
121 Crypto.fromHex hex |> Phonemic.toOb
120 Crypto.fromHex hex |> Phonemic.toOb
122
121
@@ -178,29 +177,35 @@ module App =
178 { Page = Setup
177 { Page = Setup
179 Auth = NoIdentity
178 Auth = NoIdentity
180 ServerUrl = "http://localhost:8080"
179 ServerUrl = "http://localhost:8080"
181− PollStatus = ""
180+ PollStatus = Idle
182 Contacts = []
181 Contacts = []
183 Messages = Map.empty
182 Messages = Map.empty
184 Error = None
183 Error = None
185− PollCursor = 0L
184+ PollCursor = 0L },
186− Compose = ""
187− ContactPubkey = ""
188− ContactNickname = "" },
189 [ CmdLoadState ]
185 [ CmdLoadState ]
190
186
191 // ── Update ──
187 // ── Update ──
192
188
193 let update msg model =
189 let update msg model =
194 match msg with
190 match msg with
195− | SetPage(Chat pk) ->
191+ | SetPage(Chat(pk, _)) ->
196− { model with Page = Chat pk; Compose = "" }, []
192+ { model with Page = Chat(pk, "") }, []
197− | SetPage AddContact ->
193+ | SetPage(AddContact _) ->
198− { model with Page = AddContact; ContactPubkey = ""; ContactNickname = "" }, []
194+ { model with Page = AddContact("", "") }, []
199 | SetPage page -> { model with Page = page }, []
195 | SetPage page -> { model with Page = page }, []
200
196
201− | SetCompose text -> { model with Compose = text }, []
197+ | SetCompose text ->
198+ match model.Page with
199+ | Chat(pk, _) -> { model with Page = Chat(pk, text) }, []
200+ | _ -> model, []
202− | SetContactPubkey pk -> { model with ContactPubkey = pk }, []
201+ | SetContactPubkey pk ->
202+ match model.Page with
203+ | AddContact(_, nn) -> { model with Page = AddContact(pk, nn) }, []
204+ | _ -> model, []
203− | SetContactNickname nn -> { model with ContactNickname = nn }, []
205+ | SetContactNickname nn ->
206+ match model.Page with
207+ | AddContact(pk, _) -> { model with Page = AddContact(pk, nn) }, []
208+ | _ -> model, []
204
209
205 | GenIdentity -> model, [ CmdGenIdentity ]
210 | GenIdentity -> model, [ CmdGenIdentity ]
206
211
@@ -230,21 +235,20 @@ module App =
230
235
231 | DoSend ->
236 | DoSend ->
232 match model.Page, trySession model with
237 match model.Page, trySession model with
233− | Chat pk, Some session when model.Compose <> "" ->
238+ | Chat(pk, compose), Some session when compose <> "" ->
234− let text = model.Compose
235 let id = Guid.NewGuid().ToString()
239 let id = Guid.NewGuid().ToString()
236 let outMsg =
240 let outMsg =
237 { Id = id
241 { Id = id
238− Body = text
242+ Body = compose
239 Timestamp = DateTimeOffset.UtcNow
243 Timestamp = DateTimeOffset.UtcNow
240 IsOutgoing = true }
244 IsOutgoing = true }
241
245
242 let model' =
246 let model' =
243 { model with
247 { model with
244− Compose = ""
248+ Page = Chat(pk, "")
245 Messages = model.Messages |> appendMessage pk outMsg }
249 Messages = model.Messages |> appendMessage pk outMsg }
246
250
247− model', [ CmdSend(session, pk, text, id); saveCmdMsg model' ]
251+ model', [ CmdSend(session, pk, compose, id); saveCmdMsg model' ]
248 | _ -> model, []
252 | _ -> model, []
249
253
250 | SendFailed err -> { model with Error = Some err }, []
254 | SendFailed err -> { model with Error = Some err }, []
@@ -253,10 +257,10 @@ module App =
253 let model' =
257 let model' =
254 incoming
258 incoming
255 |> List.fold
259 |> List.fold
256− (fun (m: Model) (fromPubkey, chatMsg) ->
260+ (fun (m: Model) msg ->
257 { m with
261 { m with
258− Contacts = m.Contacts |> addContactIfNew { Pubkey = fromPubkey; Nickname = truncKey fromPubkey }
262+ Contacts = m.Contacts |> addContactIfNew { Pubkey = msg.SenderPubkey; Nickname = truncKey msg.SenderPubkey }
259− Messages = m.Messages |> appendMessage fromPubkey chatMsg })
263+ Messages = m.Messages |> appendMessage msg.SenderPubkey msg.Message })
260 { model with PollStatus = status; PollCursor = newCursor }
264 { model with PollStatus = status; PollCursor = newCursor }
261
265
262 model',
266 model',
@@ -266,9 +270,8 @@ module App =
266 if not incoming.IsEmpty then saveCmdMsg model' ]
270 if not incoming.IsEmpty then saveCmdMsg model' ]
267
271
268 | DoSaveContact ->
272 | DoSaveContact ->
269− let pk, nn = model.ContactPubkey, model.ContactNickname
270 match model.Page with
273 match model.Page with
271− | AddContact when pk <> "" && nn <> "" ->
274+ | AddContact(pk, nn) when pk <> "" && nn <> "" ->
272 match tryParsePubkey pk with
275 match tryParsePubkey pk with
273 | Some bytes ->
276 | Some bytes ->
274 let contact: Contact = { Pubkey = Crypto.toHex bytes; Nickname = nn }
277 let contact: Contact = { Pubkey = Crypto.toHex bytes; Nickname = nn }
@@ -323,11 +326,12 @@ module App =
323
326
324 match envelope.Type with
327 match envelope.Type with
325 | Text ->
328 | Text ->
326− Ok(senderHex,
329+ Ok { SenderPubkey = senderHex
330+ Message =
327− { Id = envelope.Id
331+ { Id = envelope.Id
328− Body = envelope.Body
332+ Body = envelope.Body
329− Timestamp = DateTimeOffset.FromUnixTimeSeconds(payload.Timestamp)
333+ Timestamp = DateTimeOffset.FromUnixTimeSeconds(payload.Timestamp)
330− IsOutgoing = false })
334+ IsOutgoing = false } }
331 | UnknownEnvelope t -> Error $"unknown envelope type: {t}"
335 | UnknownEnvelope t -> Error $"unknown envelope type: {t}"
332 | None -> Error "decrypt failed"
336 | None -> Error "decrypt failed"
333 with ex ->
337 with ex ->
@@ -388,20 +392,14 @@ module App =
388 if not ackIds.IsEmpty then
392 if not ackIds.IsEmpty then
389 do! ackMessages session.Url session.Token ackIds
393 do! ackMessages session.Url session.Token ackIds
390
394
391− let now = nowStr ()
392−
393− let status =
394− $"[{now}] evts:{response.Events.Length} msgs:{messages.Length} errs:{errors.Length}"
395+ let status = Received(response.Events.Length, messages.Length, errors.Length, List.tryHead errors)
395− + (match errors with err :: _ -> $" [{err}]" | [] -> "")
396−
397 return PollResult(messages, status, response.Cursor)
396 return PollResult(messages, status, response.Cursor)
398 with
397 with
399 | :? TimeoutException ->
398 | :? TimeoutException ->
400− return PollResult([], "polling...", cursor)
399+ return PollResult([], Polling, cursor)
401 | ex ->
400 | ex ->
402− let now = nowStr ()
403 do! Async.Sleep 3000
401 do! Async.Sleep 3000
404− return PollResult([], $"[{now}] poll error: {ex.Message}", cursor)
402+ return PollResult([], PollError ex.Message, cursor)
405 })
403 })
406
404
407 | CmdCopyToClipboard text ->
405 | CmdCopyToClipboard text ->
@@ -428,6 +426,14 @@ module App =
428
426
429 // ── View ──
427 // ── View ──
430
428
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+
431 let private viewSetup () =
437 let private viewSetup () =
432 ContentPage(
438 ContentPage(
433 (VStack(spacing = 24.) {
439 (VStack(spacing = 24.) {
@@ -495,14 +501,6 @@ module App =
495 | Identified(_, Connecting) -> "CONNECTING"
501 | Identified(_, Connecting) -> "CONNECTING"
496 | Identified(_, Online _) -> "ONLINE"
502 | Identified(_, Online _) -> "ONLINE"
497
503
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−
506 ContentPage(
504 ContentPage(
507 ScrollView(
505 ScrollView(
508 (VStack(spacing = 8.) {
506 (VStack(spacing = 8.) {
@@ -510,22 +508,19 @@ module App =
510 Label("Conversations")
508 Label("Conversations")
511 .font(size = 24.)
509 .font(size = 24.)
512
510
513− Button("+", SetPage AddContact)
511+ Button("+", SetPage(AddContact("", "")))
514 Button("Settings", SetPage Settings)
512 Button("Settings", SetPage Settings)
515 }
513 }
516
514
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}")
522 .font(size = 10.)
516 .font(size = 10.)
523 .textColor(Colors.DimGray)
517 .textColor(Colors.DimGray)
524
518
525− if contactNames <> "" then
519+ match model.Error with
526− Label($"names: {contactNames}")
520+ | Some err ->
527− .font(size = 10.)
521+ Label(err).textColor(Colors.Red).font(size = 12.)
528− .textColor(Colors.DimGray)
522+ Button("Dismiss", DismissError)
523+ | None -> ()
529
524
530 if model.Contacts.IsEmpty then
525 if model.Contacts.IsEmpty then
531 Label("No contacts yet. Tap + to add one.")
526 Label("No contacts yet. Tap + to add one.")
@@ -536,16 +531,16 @@ module App =
536 let preview =
531 let preview =
537 messagesFor c.Pubkey model.Messages
532 messagesFor c.Pubkey model.Messages
538 |> List.tryLast
533 |> 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)
540 |> Option.defaultValue ""
535 |> Option.defaultValue ""
541
536
542− Button($"{c.Nickname}\n{preview}", SetPage(Chat c.Pubkey))
537+ Button($"{c.Nickname}\n{preview}", SetPage(Chat(c.Pubkey, "")))
543 })
538 })
544 .padding(20.)
539 .padding(20.)
545 )
540 )
546 )
541 )
547
542
548− let private viewChat model pk =
543+ let private viewChat model pk compose =
549 let name = contactName model.Contacts pk
544 let name = contactName model.Contacts pk
550 let msgs = messagesFor pk model.Messages
545 let msgs = messagesFor pk model.Messages
551
546
@@ -563,7 +558,9 @@ module App =
563 | Some err ->
558 | Some err ->
564 Label(err)
559 Label(err)
565 .textColor(Colors.Red)
560 .textColor(Colors.Red)
566− .font(size = 11.)
561+ .font(size = 12.)
562+
563+ Button("Dismiss", DismissError)
567 | None -> ()
564 | None -> ()
568
565
569 if msgs.IsEmpty then
566 if msgs.IsEmpty then
@@ -577,7 +574,7 @@ module App =
577 .font(size = 14.)
574 .font(size = 14.)
578
575
579 HStack(spacing = 8.) {
576 HStack(spacing = 8.) {
580− Entry(model.Compose, SetCompose)
577+ Entry(compose, SetCompose)
581 .placeholder("Message...")
578 .placeholder("Message...")
582
579
583 Button("Send", DoSend)
580 Button("Send", DoSend)
@@ -586,7 +583,7 @@ module App =
586 .padding(12.)
583 .padding(12.)
587 )
584 )
588
585
589− let private viewAddContact model =
586+ let private viewAddContact model pubkey nickname =
590 ContentPage(
587 ContentPage(
591 (VStack(spacing = 16.) {
588 (VStack(spacing = 16.) {
592 HStack(spacing = 8.) {
589 HStack(spacing = 8.) {
@@ -596,12 +593,18 @@ module App =
596 .font(size = 20.)
593 .font(size = 20.)
597 }
594 }
598
595
596+ match model.Error with
597+ | Some err ->
598+ Label(err).textColor(Colors.Red).font(size = 12.)
599+ Button("Dismiss", DismissError)
600+ | None -> ()
601+
599 Label("Public Key:")
602 Label("Public Key:")
600− Entry(model.ContactPubkey, SetContactPubkey)
603+ Entry(pubkey, SetContactPubkey)
601 .placeholder("sampel-palnet-...")
604 .placeholder("sampel-palnet-...")
602
605
603 Label("Nickname:")
606 Label("Nickname:")
604− Entry(model.ContactNickname, SetContactNickname)
607+ Entry(nickname, SetContactNickname)
605
608
606 Button("Save Contact", DoSaveContact)
609 Button("Save Contact", DoSaveContact)
607 .centerHorizontal()
610 .centerHorizontal()
@@ -615,8 +618,8 @@ module App =
615 | Setup -> viewSetup ()
618 | Setup -> viewSetup ()
616 | Settings -> viewSettings model
619 | Settings -> viewSettings model
617 | Conversations -> viewConversations model
620 | Conversations -> viewConversations model
618− | Chat pk -> viewChat model pk
621+ | Chat(pk, compose) -> viewChat model pk compose
619− | AddContact -> viewAddContact model
622+ | AddContact(pubkey, nickname) -> viewAddContact model pubkey nickname
620
623
621 Application() { Window(page) }
624 Application() { Window(page) }
622
625
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 @@
166 </data>
166 </data>
167 <key>Skrepka.dll</key>
167 <key>Skrepka.dll</key>
168 <data>
168 <data>
169− 17Ja2NZg0tFd466CxnE4FAYcaCU=
169+ XZqIgqY1iuk+FHTMZbQg6H06BPc=
170 </data>
170 </data>
171 <key>Skrepka.pdb</key>
171 <key>Skrepka.pdb</key>
172 <data>
172 <data>
173− HDd9myvi38aIkE3tHWkWvybpITE=
173+ jGsllzUykGPLD645ovFAhjzyj0U=
174 </data>
174 </data>
175 <key>Sodium.Core.dll</key>
175 <key>Sodium.Core.dll</key>
176 <data>
176 <data>
@@ -1393,14 +1393,14 @@
1393 <dict>
1393 <dict>
1394 <key>hash2</key>
1394 <key>hash2</key>
1395 <data>
1395 <data>
1396− sg0ozQt55G8nT4JyqSmTfQ0OiW2Hh79ThNsHjl9JMrg=
1396+ bxgl7DOK4AVvfqG7HqK1vCim2a91AGyHoW/pw+rbnjQ=
1397 </data>
1397 </data>
1398 </dict>
1398 </dict>
1399 <key>Skrepka.pdb</key>
1399 <key>Skrepka.pdb</key>
1400 <dict>
1400 <dict>
1401 <key>hash2</key>
1401 <key>hash2</key>
1402 <data>
1402 <data>
1403− IgABOviFh2f026awA/Fpe6il6FzUtRP9ssoE1G6a39E=
1403+ 3Fifs0NPR+poxKmLRf5cxvJCm1fk9qFg9gOZSl1VkC0=
1404 </data>
1404 </data>
1405 </dict>
1405 </dict>
1406 <key>Sodium.Core.dll</key>
1406 <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 @@
251 <dict>
251 <dict>
252 <key>hash2</key>
252 <key>hash2</key>
253 <data>
253 <data>
254− 3BE8YIxbdGoJrALNRoC50MYvlg4zsLOk7ap2LT2m948=
254+ sJGkuR5gxMR4CxhoKVU1LJTdCM3P3wmaAaPscSiCdwk=
255 </data>
255 </data>
256 </dict>
256 </dict>
257 <key>MonoBundle/Skrepka.pdb</key>
257 <key>MonoBundle/Skrepka.pdb</key>
258 <dict>
258 <dict>
259 <key>hash2</key>
259 <key>hash2</key>
260 <data>
260 <data>
261− 6rH6M4XCjLG1C6imAiKYY4h+PeM762EdbZ4CI+q0O0g=
261+ ANJi//INYTZtSEEDMIzRTGBB9feNGEHg+ZFulqWLaDc=
262 </data>
262 </data>
263 </dict>
263 </dict>
264 <key>MonoBundle/Sodium.Core.dll</key>
264 <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
10[<assembly: System.Reflection.AssemblyCompanyAttribute("Skrepka")>]
10[<assembly: System.Reflection.AssemblyCompanyAttribute("Skrepka")>]
11[<assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")>]
11[<assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")>]
12[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
12[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
13−[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+6b1d74aecd0ed0fb71c97a95acff19b5fb9bf149")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+e3148dde3e9c36905f2c3f85861acc4776bc1da2")>]
14[<assembly: System.Reflection.AssemblyProductAttribute("Skrepka")>]
14[<assembly: System.Reflection.AssemblyProductAttribute("Skrepka")>]
15[<assembly: System.Reflection.AssemblyTitleAttribute("Skrepka")>]
15[<assembly: System.Reflection.AssemblyTitleAttribute("Skrepka")>]
16[<assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")>]
16[<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 @@
334/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.genruntimeconfig.cache
334/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.genruntimeconfig.cache
335/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/ref/Skrepka.dll
335/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/ref/Skrepka.dll
336/Users/iko/Developer/skrepka/client/bin/Debug/net10.0-ios/iossimulator-arm64/LiteDB.dll
336/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
10[<assembly: System.Reflection.AssemblyCompanyAttribute("Skrepka")>]
10[<assembly: System.Reflection.AssemblyCompanyAttribute("Skrepka")>]
11[<assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")>]
11[<assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")>]
12[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
12[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
13−[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+e6d52540f7a36ceb966de06191cfd7c3d223c800")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+e3148dde3e9c36905f2c3f85861acc4776bc1da2")>]
14[<assembly: System.Reflection.AssemblyProductAttribute("Skrepka")>]
14[<assembly: System.Reflection.AssemblyProductAttribute("Skrepka")>]
15[<assembly: System.Reflection.AssemblyTitleAttribute("Skrepka")>]
15[<assembly: System.Reflection.AssemblyTitleAttribute("Skrepka")>]
16[<assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")>]
16[<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 != ""
48-- Types
48-- Types
49-- ============================================
49-- ============================================
50
50
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}}
60
52
61type GossipEventType = GossipOnline {} | GossipOffline {}
53type GossipEventType = GossipOnline {} | GossipOffline {}
62
54
@@ -68,7 +60,9 @@ impl ToJSON GossipEventType where
68impl FromJSON GossipEventType where
60impl FromJSON GossipEventType where
69 parseJson t =
61 parseJson t =
70 let s : Text = parseJson t in
62 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 {}
72
66
73isGossipOnline = \et -> case et of GossipOnline {} -> True {}; _ -> False {}
67isGossipOnline = \et -> case et of GossipOnline {} -> True {}; _ -> False {}
74
68
@@ -93,6 +87,7 @@ impl FromJSON MessageStatus where
93 else if s == "rejected" then MsRejected {}
87 else if s == "rejected" then MsRejected {}
94 else if s == "delivered" then MsDelivered {}
88 else if s == "delivered" then MsDelivered {}
95 else if s == "federated" then MsFederated {}
89 else if s == "federated" then MsFederated {}
90+ else if s == "queued" then MsQueued {}
96 else MsQueued {}
91 else MsQueued {}
97
92
98type PubkeyHex = Text where \s -> isValidHex s && not (tooLong s maxPubkeyLen) && s == normalizeHex s
93type PubkeyHex = Text where \s -> isValidHex s && not (tooLong s maxPubkeyLen) && s == normalizeHex s
@@ -488,20 +483,20 @@ mergeByKey = \sameKey getFreshness initial items ->
488
483
489mergeByPubkeyServer = mergeByKey samePubkeyServer (\x -> x.expiresAt)
484mergeByPubkeyServer = mergeByKey samePubkeyServer (\x -> x.expiresAt)
490
485
491−removeOffline = \presenceSet offlineOnlyKeys trustedOffline t ->
486+removeOffline = \presenceSet trustedOffline t ->
492 diff presenceSet (do
487 diff presenceSet (do
493 p <- presenceSet
488 p <- presenceSet
494− k <- offlineOnlyKeys
489+ o <- trustedOffline
495− where samePubkeyServer p k && any trustedOffline (\o -> samePubkeyServer o k && (if o.timestamp > t then t else o.timestamp) > p.timestamp)
490+ where samePubkeyServer p o && (if o.timestamp > t then t else o.timestamp) > p.timestamp
496 yield p)
491 yield p)
497
492
498−capGossipPresence = \afterOffline appliedOnline trustedDeduped presence offlineOnlyKeys trustedOffline t ->
493+capGossipPresence = \afterOffline appliedOnline trustedDeduped presence trustedOffline t ->
499 if count afterOffline <= maxPresenceEntries
494 if count afterOffline <= maxPresenceEntries
500 then {finalPresence: afterOffline, appliedOnline: appliedOnline, withinCapacity: True {}}
495 then {finalPresence: afterOffline, appliedOnline: appliedOnline, withinCapacity: True {}}
501 else
496 else
502 let updatesOnly = do e <- trustedDeduped; where any presence (\p -> samePubkeyServer p e); yield e in
497 let updatesOnly = do e <- trustedDeduped; where any presence (\p -> samePubkeyServer p e); yield e in
503 let updatesOnlyMerged = mergeByPubkeyServer presence updatesOnly in
498 let updatesOnlyMerged = mergeByPubkeyServer presence updatesOnly in
504− let updatesOnlyAfterOffline = removeOffline updatesOnlyMerged offlineOnlyKeys trustedOffline t in
499+ let updatesOnlyAfterOffline = removeOffline updatesOnlyMerged trustedOffline t in
505 let reclaimSlots = maxPresenceEntries - count updatesOnlyAfterOffline in
500 let reclaimSlots = maxPresenceEntries - count updatesOnlyAfterOffline in
506 let newEntries = do e <- trustedDeduped; where none presence (\p -> samePubkeyServer p e); yield e in
501 let newEntries = do e <- trustedDeduped; where none presence (\p -> samePubkeyServer p e); yield e in
507 let reclaimedEntries = takeN newEntries reclaimSlots in
502 let reclaimedEntries = takeN newEntries reclaimSlots in
@@ -512,10 +507,7 @@ capGossipPresence = \afterOffline appliedOnline trustedDeduped presence offlineO
512 withinCapacity: False {}}
507 withinCapacity: False {}}
513
508
514countMessagesFor = \messages toKey t ->
509countMessagesFor = \messages toKey t ->
515− count (do
516− m <- messages
517− where m.toKey == toKey && m.receivedAt >= t - messageMaxAge
510+ fold (\n m -> if m.toKey == toKey && m.receivedAt >= t - messageMaxAge then n + 1 else n) 0 messages
518− yield m)
519
511
520
512
521validGossipEvents : [GossipEvent] -> Timestamp -> [ValidGossipEntry]
513validGossipEvents : [GossipEvent] -> Timestamp -> [ValidGossipEntry]
@@ -577,13 +569,18 @@ maybePrune = \t -> do
577 set *lastPrunedAt = t
569 set *lastPrunedAt = t
578 pruneExpired t
570 pruneExpired t
579
571
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+
580forEachBatch = \items batchSize process ->
578forEachBatch = \items batchSize process ->
581 if count items == 0 then yield {}
579 if count items == 0 then yield {}
582 else do
580 else do
583− let batch = takeN items batchSize
581+ let split = splitAtN items batchSize
584− let rest = diff items batch
585− process batch
582+ process split.batch
586− forEachBatch rest batchSize process
583+ forEachBatch split.rest batchSize process
587
584
588sendBatchedGossipToServer : ServerName -> [GossipEvent] -> Int -> IO {network} {}
585sendBatchedGossipToServer : ServerName -> [GossipEvent] -> Int -> IO {network} {}
589sendBatchedGossipToServer = \server allEvents hops ->
586sendBatchedGossipToServer = \server allEvents hops ->
@@ -721,7 +718,7 @@ pollOnce = \token cursor -> do
721 let limitedMsgs = boundedSmallest matchingMsgs maxPollEvents (\m -> m.receivedAt)
718 let limitedMsgs = boundedSmallest matchingMsgs maxPollEvents (\m -> m.receivedAt)
722 let evts = do
719 let evts = do
723 m <- limitedMsgs
720 m <- limitedMsgs
724− yield {id: m.messageId, eventType: PollMessage {},
721+ yield {id: m.messageId, eventType: "message",
725 payload: {toKey: m.toKey, fromKey: m.fromKey,
722 payload: {toKey: m.toKey, fromKey: m.fromKey,
726 encryptedBlob: m.encryptedBlob,
723 encryptedBlob: m.encryptedBlob,
727 timestamp: m.timestamp}}
724 timestamp: m.timestamp}}
@@ -931,7 +928,7 @@ serve = \req -> case req of
931 else do
928 else do
932 let allValidEntries = validGossipEvents events t
929 let allValidEntries = validGossipEvents events t
933 let onlineEntries = do e <- allValidEntries; where isGossipOnline e.eventType; yield e
930 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
935 let trustedOffline = do e <- offlineEntries; where e.server == fromServer; yield e
932 let trustedOffline = do e <- offlineEntries; where e.server == fromServer; yield e
936 let filteredOnline = do
933 let filteredOnline = do
937 e <- onlineEntries
934 e <- onlineEntries
@@ -954,13 +951,9 @@ serve = \req -> case req of
954 where none presence (\p -> samePubkeyServer p e && p.expiresAt >= e.expiresAt)
951 where none presence (\p -> samePubkeyServer p e && p.expiresAt >= e.expiresAt)
955 yield {pubkey: e.pubkey, server: e.server}
952 yield {pubkey: e.pubkey, server: e.server}
956 let merged = mergeByPubkeyServer presence trustedDeduped
953 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
962 let appliedOffline = do p <- diff merged afterOffline; yield {pubkey: p.pubkey, server: p.server}
955 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
964 set *presence = capResult.finalPresence
957 set *presence = capResult.finalPresence
965 let gossipPeerServers = collectPeerServers capResult.finalPresence (\p -> p.server)
958 let gossipPeerServers = collectPeerServers capResult.finalPresence (\p -> p.server)
966 yield {appliedKeys: union capResult.appliedOnline appliedOffline, pr: pr, gossipPeerServers: gossipPeerServers, withinCapacity: capResult.withinCapacity}
959 yield {appliedKeys: union capResult.appliedOnline appliedOffline, pr: pr, gossipPeerServers: gossipPeerServers, withinCapacity: capResult.withinCapacity}