on master help sign in
Simplify code and improve type safety: replace manual JSON parsing with typed deserialization, use JsonPropertyName
61c0ab0iko
client/ApiClient.fs+53−51
@@ -11,9 +11,26 @@ module ApiClient =
11 type EventPayload = { EncryptedBlob: string; Timestamp: int64 }
11 type EventPayload = { EncryptedBlob: string; Timestamp: int64 }
12 type PollEvent = { Id: string; Payload: EventPayload }
12 type PollEvent = { Id: string; Payload: EventPayload }
13
13
14+ [<CLIMutable>]
14 type SendResult = { Status: string; MessageId: string }
15 type SendResult = { Status: string; MessageId: string }
16+
15 type PollResponse = { Events: PollEvent list; Cursor: uint64 }
17 type PollResponse = { Events: PollEvent list; Cursor: uint64 }
16
18
19+ [<CLIMutable>]
20+ type ChallengeResponse = { challenge: string }
21+
22+ [<CLIMutable>]
23+ type VerifyResponse = { token: string }
24+
25+ [<CLIMutable>]
26+ type PollPayloadDto = { encryptedBlob: string; timestamp: JsonElement }
27+
28+ [<CLIMutable>]
29+ type PollEventDto = { id: string; eventType: string; payload: PollPayloadDto }
30+
31+ [<CLIMutable>]
32+ type PollResponseDto = { cursor: uint64; events: PollEventDto array }
33+
17 /// Awaits a Task without converting TaskCanceledException to F# async
34 /// Awaits a Task without converting TaskCanceledException to F# async
18 /// cancellation (which bypasses try...with). Re-raises it as a regular exception.
35 /// cancellation (which bypasses try...with). Re-raises it as a regular exception.
19 let private awaitTask (t: Task<'T>) : Async<'T> =
36 let private awaitTask (t: Task<'T>) : Async<'T> =
@@ -24,12 +41,19 @@ module ApiClient =
24 else ok t.Result)
41 else ok t.Result)
25 |> ignore)
42 |> ignore)
26
43
44+ let private jsonOpts = JsonSerializerOptions(PropertyNameCaseInsensitive = true)
45+
27 let private client =
46 let private client =
28 let c = new HttpClient()
47 let c = new HttpClient()
29 c.Timeout <- TimeSpan.FromSeconds(60.)
48 c.Timeout <- TimeSpan.FromSeconds(60.)
30 c
49 c
31
50
51+ let private pollClient =
52+ let c = new HttpClient()
53+ c.Timeout <- TimeSpan.FromSeconds(120.)
54+ c
55+
32− let private postJsonWith (httpClient: HttpClient) (url: string) (body: string) (token: string option) =
56+ let private sendRequest (httpClient: HttpClient) (url: string) (body: string) (token: string option) =
33 async {
57 async {
34 let request = new HttpRequestMessage(HttpMethod.Post, url)
58 let request = new HttpRequestMessage(HttpMethod.Post, url)
35 request.Content <- new StringContent(body, Encoding.UTF8, "application/json")
59 request.Content <- new StringContent(body, Encoding.UTF8, "application/json")
@@ -39,82 +63,60 @@ module ApiClient =
39
63
40 let! response = httpClient.SendAsync(request) |> awaitTask
64 let! response = httpClient.SendAsync(request) |> awaitTask
41 let! text = response.Content.ReadAsStringAsync() |> awaitTask
65 let! text = response.Content.ReadAsStringAsync() |> awaitTask
42− return JsonDocument.Parse(text)
66+ use doc = JsonDocument.Parse(text)
43− }
44
67
45− let private postJson url body token = postJsonWith client url body token
46−
47− let private expectObject (doc: JsonDocument) (endpoint: string) =
48− let root = doc.RootElement
68+ match doc.RootElement.TryGetProperty("error") with
49−
69+ | true, err -> return failwith $"{url}: {err.GetString()}"
50− if root.ValueKind <> JsonValueKind.Object then
70+ | _ -> return text
51− failwith $"{endpoint}: unexpected response: {root.GetRawText()}"
71+ }
52
72
53− match root.TryGetProperty("error") with
73+ let private postJson<'T> httpClient url body token =
54− | true, err -> failwith $"{endpoint}: server error: {err.GetString()}"
74+ async {
55− | _ -> root
75+ let! text = sendRequest httpClient url body token
76+ return JsonSerializer.Deserialize<'T>(text, jsonOpts)
77+ }
56
78
57 let requestChallenge (serverUrl: string) (pubkeyHex: string) =
79 let requestChallenge (serverUrl: string) (pubkeyHex: string) =
58 async {
80 async {
59 let body = JsonSerializer.Serialize({| pubkey = pubkeyHex |})
81 let body = JsonSerializer.Serialize({| pubkey = pubkeyHex |})
60− let! doc = postJson $"{serverUrl}/auth/challenge" body None
82+ let! response = postJson<ChallengeResponse> client $"{serverUrl}/auth/challenge" body None
61− let root = expectObject doc "/auth/challenge"
62− return root.GetProperty("challenge").GetString()
83+ return response.challenge
63 }
84 }
64
85
65 let verify (serverUrl: string) (pubkeyHex: string) (challenge: string) (signatureHex: string) =
86 let verify (serverUrl: string) (pubkeyHex: string) (challenge: string) (signatureHex: string) =
66 async {
87 async {
67 let body = JsonSerializer.Serialize({| pubkey = pubkeyHex; challenge = challenge; signature = signatureHex |})
88 let body = JsonSerializer.Serialize({| pubkey = pubkeyHex; challenge = challenge; signature = signatureHex |})
68−
69− let! doc = postJson $"{serverUrl}/auth/verify" body None
89+ let! response = postJson<VerifyResponse> client $"{serverUrl}/auth/verify" body None
70− let root = expectObject doc "/auth/verify"
71− let token = root.GetProperty("token").GetString()
72− if String.IsNullOrEmpty(token) then failwith "Authentication rejected by server"
90+ if String.IsNullOrEmpty(response.token) then failwith "Authentication rejected by server"
73− return token
91+ return response.token
74 }
92 }
75
93
76 let sendMessage (serverUrl: string) (token: string) (toHex: string) (blobHex: string) (timestamp: int64) =
94 let sendMessage (serverUrl: string) (token: string) (toHex: string) (blobHex: string) (timestamp: int64) =
77 async {
95 async {
78 let body = JsonSerializer.Serialize({| ``to`` = toHex; encryptedBlob = blobHex; timestamp = timestamp |})
96 let body = JsonSerializer.Serialize({| ``to`` = toHex; encryptedBlob = blobHex; timestamp = timestamp |})
79−
80− let! doc = postJson $"{serverUrl}/messages" body (Some token)
97+ return! postJson<SendResult> client $"{serverUrl}/messages" body (Some token)
81− let root = expectObject doc "/messages"
82− return
83− { Status = root.GetProperty("status").GetString()
84− MessageId = root.GetProperty("messageId").GetString() }
85 }
98 }
86
99
87− let private pollClient =
88− let c = new HttpClient()
89− c.Timeout <- TimeSpan.FromSeconds(120.)
90− c
91−
92 let poll (serverUrl: string) (token: string) (cursor: uint64) =
100 let poll (serverUrl: string) (token: string) (cursor: uint64) =
93 async {
101 async {
94 let body = JsonSerializer.Serialize({| cursor = cursor; timeout = 30000 |})
102 let body = JsonSerializer.Serialize({| cursor = cursor; timeout = 30000 |})
95− let! doc = postJsonWith pollClient $"{serverUrl}/poll" body (Some token)
103+ let! response = postJson<PollResponseDto> pollClient $"{serverUrl}/poll" body (Some token)
96− let root = expectObject doc "/poll"
97− let cursor = root.GetProperty("cursor").GetUInt64()
98−
99− let events = root.GetProperty("events")
100
104
101 let events =
105 let events =
102− [ for e in events.EnumerateArray() do
106+ [ for e in response.events do
103− if e.ValueKind = JsonValueKind.Object
104− && e.GetProperty("eventType").GetString() = "message" then
107+ if e.eventType = "message" then
105− let p = e.GetProperty("payload")
108+ let ts =
106− let ts = p.GetProperty("timestamp")
109+ if e.payload.timestamp.ValueKind = JsonValueKind.String
107− yield
110+ then Int64.Parse(e.payload.timestamp.GetString())
108− { Id = e.GetProperty("id").GetString()
111+ else e.payload.timestamp.GetInt64()
109− Payload =
112+ { Id = e.id
110− { EncryptedBlob = p.GetProperty("encryptedBlob").GetString()
113+ Payload = { EncryptedBlob = e.payload.encryptedBlob; Timestamp = ts } } ]
111− Timestamp = if ts.ValueKind = JsonValueKind.String then Int64.Parse(ts.GetString()) else ts.GetInt64() } } ]
114+
112−
113− return { Events = events; Cursor = cursor }
115+ return { Events = events; Cursor = response.cursor }
114 }
116 }
115
117
116 let ackMessages (serverUrl: string) (token: string) (messageIds: string list) =
118 let ackMessages (serverUrl: string) (token: string) (messageIds: string list) =
117 async {
119 async {
118 let body = JsonSerializer.Serialize({| messageIds = messageIds |})
120 let body = JsonSerializer.Serialize({| messageIds = messageIds |})
119− do! postJson $"{serverUrl}/messages/ack" body (Some token) |> Async.Ignore
121+ do! sendRequest client $"{serverUrl}/messages/ack" body (Some token) |> Async.Ignore
120 }
122 }
client/App.fs+4−3
@@ -2,6 +2,7 @@ namespace Skrepka
2
2
3open System
3open System
4open System.Text.Json
4open System.Text.Json
5+open System.Text.Json.Serialization
5open Fabulous
6open Fabulous
6open Fabulous.Maui
7open Fabulous.Maui
7open System.Collections.Generic
8open System.Collections.Generic
@@ -23,7 +24,7 @@ module App =
23
24
24 [<CLIMutable>]
25 [<CLIMutable>]
25 type TextEnvelope =
26 type TextEnvelope =
26− { ``type``: string
27+ { [<JsonPropertyName("type")>] Type: string
27 id: string
28 id: string
28 timestamp: int64
29 timestamp: int64
29 body: string }
30 body: string }
@@ -311,7 +312,7 @@ module App =
311 | Some(plaintext, senderHex) ->
312 | Some(plaintext, senderHex) ->
312 let envelope = JsonSerializer.Deserialize<TextEnvelope>(plaintext)
313 let envelope = JsonSerializer.Deserialize<TextEnvelope>(plaintext)
313
314
314− if envelope.``type`` = "text" then
315+ if envelope.Type = "text" then
315 Ok(senderHex,
316 Ok(senderHex,
316 { Id = envelope.id
317 { Id = envelope.id
317 Body = envelope.body
318 Body = envelope.body
@@ -346,7 +347,7 @@ module App =
346 let ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
347 let ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
347
348
348 let payload =
349 let payload =
349− JsonSerializer.Serialize({ ``type`` = "text"; id = messageId; timestamp = ts; body = text }: TextEnvelope)
350+ JsonSerializer.Serialize({ Type = "text"; id = messageId; timestamp = ts; body = text }: TextEnvelope)
350
351
351 let blob = Crypto.encrypt session.Identity.PrivKey recipPub payload
352 let blob = Crypto.encrypt session.Identity.PrivKey recipPub payload
352 let blobHex = Crypto.toHex blob
353 let blobHex = Crypto.toHex blob
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
@@ -162,11 +162,11 @@
162 </data>
162 </data>
163 <key>Skrepka.dll</key>
163 <key>Skrepka.dll</key>
164 <data>
164 <data>
165− HO+rGAQB+KP+O6GLdnrpg4gXNTo=
165+ Fx5eEqogOKsTrWA3kWp16d3fSyU=
166 </data>
166 </data>
167 <key>Skrepka.pdb</key>
167 <key>Skrepka.pdb</key>
168 <data>
168 <data>
169− gVY2miH2VNwTxjVewvuMbzDx/GU=
169+ HqhmdetfGb7BH5l9fuwhGdebTOE=
170 </data>
170 </data>
171 <key>Sodium.Core.dll</key>
171 <key>Sodium.Core.dll</key>
172 <data>
172 <data>
@@ -1382,14 +1382,14 @@
1382 <dict>
1382 <dict>
1383 <key>hash2</key>
1383 <key>hash2</key>
1384 <data>
1384 <data>
1385− D1zjN3wi4JGWYPuOGUv3OatZGfdLQhYyE2a3FQRO3Sk=
1385+ FjsECHINikzgAyKIKVfKchjBJ13FDPE/H9z1BXBRz0w=
1386 </data>
1386 </data>
1387 </dict>
1387 </dict>
1388 <key>Skrepka.pdb</key>
1388 <key>Skrepka.pdb</key>
1389 <dict>
1389 <dict>
1390 <key>hash2</key>
1390 <key>hash2</key>
1391 <data>
1391 <data>
1392− Qdm9JVWHZq1+ocF6a6hRUFeaSuFiylZp+ykwgIfTQ3A=
1392+ 3GjYcjVCL6cSFSxxBQY9gaQpUEE77fX5bueUq6AVhZQ=
1393 </data>
1393 </data>
1394 </dict>
1394 </dict>
1395 <key>Sodium.Core.dll</key>
1395 <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
@@ -240,14 +240,14 @@
240 <dict>
240 <dict>
241 <key>hash2</key>
241 <key>hash2</key>
242 <data>
242 <data>
243− iGYj1YzZ1prtD0NbQ3DXzBIxx8GfZCXBBkeG+NhAlAg=
243+ Tb9gTNQn0vC6xYnphz/SPqKOypsS1ii7ovZ7S6s4QoY=
244 </data>
244 </data>
245 </dict>
245 </dict>
246 <key>MonoBundle/Skrepka.pdb</key>
246 <key>MonoBundle/Skrepka.pdb</key>
247 <dict>
247 <dict>
248 <key>hash2</key>
248 <key>hash2</key>
249 <data>
249 <data>
250− gc3cHrHWiW0qjPSnbfHqE/s8LkMQGikr2nzPGAaHEgE=
250+ k0t0cbTVWz/CfZ4rlDzYjSX4lniTHLZRvz2UrUCSZdM=
251 </data>
251 </data>
252 </dict>
252 </dict>
253 <key>MonoBundle/Sodium.Core.dll</key>
253 <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+79a607dd3a4773a3bae1ef20240a704c153f91da")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+f11ecd2201e52dce49ef2d913500531961808e00")>]
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−2cd69f5b98b69e1a72e3f4bd87ef1f300a712fcc06d03dda524b6c00fdfe5200
1+07f32a66419e75da06f8eeb4cc8f91961c7616db00e976ebd52190a10f03b26f
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+22
@@ -333,3 +333,25 @@
333/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb
333/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb
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/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Fabulous.stamp
337+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Fabulous.MauiControls.stamp
338+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/FSharp.Core.stamp
339+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Configuration.Abstractions.stamp
340+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Configuration.stamp
341+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.DependencyInjection.Abstractions.stamp
342+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.DependencyInjection.stamp
343+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Diagnostics.Abstractions.stamp
344+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.FileProviders.Abstractions.stamp
345+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Hosting.Abstractions.stamp
346+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Logging.Abstractions.stamp
347+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Logging.stamp
348+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Options.stamp
349+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Primitives.stamp
350+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.Compatibility.stamp
351+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.stamp
352+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.Xaml.stamp
353+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.stamp
354+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Essentials.stamp
355+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Graphics.stamp
356+/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Sodium.Core.stamp
357+/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/79a607dd3a4773a3bae1ef20240a704c153f91da/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/f11ecd2201e52dce49ef2d913500531961808e00/*"}}
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+79a607dd3a4773a3bae1ef20240a704c153f91da")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+f11ecd2201e52dce49ef2d913500531961808e00")>]
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−20452108ea7920252e262f31e3d85af043c1d46fd7b19b9a54754e30a319f325
1+90103b2909e2d6c308fd0241aa80b308b414d7047eeca8cef6ba5f4642cdf15a
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/79a607dd3a4773a3bae1ef20240a704c153f91da/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/f11ecd2201e52dce49ef2d913500531961808e00/*"}}
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+22−27
@@ -626,7 +626,7 @@ retryPendingForwards = \pending -> do
626 retryForwardsForServer (do x <- pending; where x.toServer == server; yield x) maxForwardRetriesPerServer)
626 retryForwardsForServer (do x <- pending; where x.toServer == server; yield x) maxForwardRetriesPerServer)
627 yield {}
627 yield {}
628
628
629−sendBatchedAcksToServer : [{messageId: MessageIdHex, toKey: PubkeyHex, originServer: Text, | a}] -> Text -> IO {network} {}
629+sendBatchedAcksToServer : [{messageId: MessageIdHex, toKey: PubkeyHex, originServer: ServerName, | a}] -> ServerName -> IO {network} {}
630sendBatchedAcksToServer = \serverAcks server ->
630sendBatchedAcksToServer = \serverAcks server ->
631 forEachBatch serverAcks maxMessageIds (\batch -> do
631 forEachBatch serverAcks maxMessageIds (\batch -> do
632 result <- fetch (federationScheme ++ server) (FederationAck {
632 result <- fetch (federationScheme ++ server) (FederationAck {
@@ -643,7 +643,7 @@ sendBatchedAcksToServer = \serverAcks server ->
643 yield a
643 yield a
644 Err _ -> yield {})
644 Err _ -> yield {})
645
645
646−sendAcksAndClean : [{messageId: MessageIdHex, toKey: PubkeyHex, originServer: Text, | a}] -> [Text] -> IO {network} {}
646+sendAcksAndClean : [{messageId: MessageIdHex, toKey: PubkeyHex, originServer: ServerName, | a}] -> [ServerName] -> IO {network} {}
647sendAcksAndClean = \pendingAcks servers ->
647sendAcksAndClean = \pendingAcks servers ->
648 sendToEach servers (\server ->
648 sendToEach servers (\server ->
649 sendBatchedAcksToServer (do a <- pendingAcks; where a.originServer == server; yield a) server)
649 sendBatchedAcksToServer (do a <- pendingAcks; where a.originServer == server; yield a) server)
@@ -668,15 +668,15 @@ pollOnce = \token cursor -> do
668 t <- now
668 t <- now
669 atomic do
669 atomic do
670 sessions <- *sessions
670 sessions <- *sessions
671− seqCounter <- *seqCounter
672 let auth = authenticateSession sessions token t
671 let auth = authenticateSession sessions token t
673− let cursorCeil = if seqCounter > t then seqCounter else t
674− let clampedCursor = if cursor < 0 then 0 else if cursor > cursorCeil then cursorCeil else cursor
675 case auth of
672 case auth of
676 Unauthorized {} ->
673 Unauthorized {} ->
677 yield {events: [], cursor: -1, authorized: False {}}
674 yield {events: [], cursor: -1, authorized: False {}}
678 Authorized {pubkey} -> do
675 Authorized {pubkey} -> do
676+ seqCounter <- *seqCounter
679 messages <- *messages
677 messages <- *messages
678+ let cursorCeil = if seqCounter > t then seqCounter else t
679+ let clampedCursor = if cursor < 0 then 0 else if cursor > cursorCeil then cursorCeil else cursor
680 let matchingMsgs = do
680 let matchingMsgs = do
681 m <- messages
681 m <- messages
682 where m.toKey == pubkey
682 where m.toKey == pubkey
@@ -808,7 +808,7 @@ serve = \req -> case req of
808 let auth = authenticateSession sessions (extractToken authorization) t
808 let auth = authenticateSession sessions (extractToken authorization) t
809 case auth of
809 case auth of
810 Unauthorized {} ->
810 Unauthorized {} ->
811− yield {status: "unauthorized", shouldInsert: False {}, remoteServers: [], fromKey: "", expiredLocal: pr.expiredLocal, peerServers: pr.peerServers, forwardsAtCapacity: False {}}
811+ yield {status: "unauthorized", shouldInsert: False {}, remoteServers: [], fromKey: "", expiredLocal: pr.expiredLocal, peerServers: pr.peerServers}
812 Authorized {pubkey: fromKey} -> do
812 Authorized {pubkey: fromKey} -> do
813 messages <- *messages
813 messages <- *messages
814 presence <- *presence
814 presence <- *presence
@@ -825,17 +825,15 @@ serve = \req -> case req of
825 let localOnline = any toPresence (\p -> p.server == serverHostN)
825 let localOnline = any toPresence (\p -> p.server == serverHostN)
826 let remoteServers = do p <- toPresence; where not (p.server == serverHostN); yield p.server
826 let remoteServers = do p <- toPresence; where not (p.server == serverHostN); yield p.server
827 forwards <- *forwards
827 forwards <- *forwards
828− let forwardsAtCapacity = count forwards >= maxForwardsEntries
828+ let forwardServers = takeN remoteServers (maxForwardsEntries - count forwards)
829 let status = if not shouldInsert then "rejected"
829 let status = if not shouldInsert then "rejected"
830 else if localOnline then "delivered"
830 else if localOnline then "delivered"
831− else if count remoteServers > 0 && federationEnabled && not forwardsAtCapacity then "federated"
831+ else if federationEnabled && count forwardServers > 0 then "federated"
832 else "queued"
832 else "queued"
833− let forwardSlots = maxForwardsEntries - count forwards
834− let forwardServers = takeN remoteServers forwardSlots
835− set *forwards = if shouldInsert && not forwardsAtCapacity && count forwardServers > 0 && federationEnabled
833+ set *forwards = if shouldInsert && federationEnabled && count forwardServers > 0
836 then union forwards (do s <- forwardServers; yield {messageId: msgId, toServer: s, createdAt: t, toKey: to, fromKey: fromKey, encryptedBlob: encryptedBlob, timestamp: clampedTs})
834 then union forwards (do s <- forwardServers; yield {messageId: msgId, toServer: s, createdAt: t, toKey: to, fromKey: fromKey, encryptedBlob: encryptedBlob, timestamp: clampedTs})
837 else forwards
835 else forwards
838− yield {status: status, shouldInsert: shouldInsert, remoteServers: remoteServers, fromKey: fromKey, expiredLocal: pr.expiredLocal, peerServers: pr.peerServers, forwardsAtCapacity: forwardsAtCapacity}
836+ yield {status: status, shouldInsert: shouldInsert, remoteServers: remoteServers, fromKey: fromKey, expiredLocal: pr.expiredLocal, peerServers: pr.peerServers}
839 fork (do
837 fork (do
840 when (count result.remoteServers > 0 && result.shouldInsert && federationEnabled)
838 when (count result.remoteServers > 0 && result.shouldInsert && federationEnabled)
841 (sendToEach result.remoteServers (\server -> do
839 (sendToEach result.remoteServers (\server -> do
@@ -846,7 +844,7 @@ serve = \req -> case req of
846 case fwdResult of
844 case fwdResult of
847 Ok _ -> removeForward msgId server
845 Ok _ -> removeForward msgId server
848 Err _ -> yield {}))
846 Err _ -> yield {}))
849− when (result.forwardsAtCapacity && count result.remoteServers > 0 && result.shouldInsert)
847+ when (result.status == "queued" && count result.remoteServers > 0 && result.shouldInsert && federationEnabled)
850 (println ("sendMessage: forwards at capacity (" ++ show maxForwardsEntries ++ "), skipping forward tracking"))
848 (println ("sendMessage: forwards at capacity (" ++ show maxForwardsEntries ++ "), skipping forward tracking"))
851 broadcastOfflineGossip t result.peerServers result.expiredLocal)
849 broadcastOfflineGossip t result.peerServers result.expiredLocal)
852 yield (respond {status: result.status, messageId: if result.shouldInsert then msgId else ""})
850 yield (respond {status: result.status, messageId: if result.shouldInsert then msgId else ""})
@@ -953,9 +951,14 @@ serve = \req -> case req of
953 e <- onlineEntries
951 e <- onlineEntries
954 where none offlineEntries (\o -> o.server == fromServer && samePubkeyServer o e && o.timestamp > e.timestamp)
952 where none offlineEntries (\o -> o.server == fromServer && samePubkeyServer o e && o.timestamp > e.timestamp)
955 yield e
953 yield e
956− let affectedKeys = do
954+ let filteredOffline = do
957− e <- allValidEntries
955+ e <- offlineEntries
958− yield {pubkey: e.pubkey, server: e.server}
956+ where none onlineEntries (\o -> samePubkeyServer o e && o.timestamp > e.timestamp)
957+ yield e
958+ let trustedOffline = do
959+ e <- offlineEntries
960+ where e.server == fromServer
961+ yield e
959 let deduped = mergeByPubkeyServer [] filteredOnline
962 let deduped = mergeByPubkeyServer [] filteredOnline
960 gossipResult <- atomic do
963 gossipResult <- atomic do
961 pr <- maybePrune t
964 pr <- maybePrune t
@@ -964,19 +967,15 @@ serve = \req -> case req of
964 e <- deduped
967 e <- deduped
965 where e.server == fromServer || any presence (\p -> p.server == e.server && p.expiresAt > t)
968 where e.server == fromServer || any presence (\p -> p.server == e.server && p.expiresAt > t)
966 yield e
969 yield e
967− let trustedOffline = do
968− e <- offlineEntries
969− where e.server == fromServer
970− yield e
971 let appliedOnline = do
970 let appliedOnline = do
972 e <- trustedDeduped
971 e <- trustedDeduped
973 where none presence (\p -> samePubkeyServer p e && p.expiresAt >= e.expiresAt)
972 where none presence (\p -> samePubkeyServer p e && p.expiresAt >= e.expiresAt)
974 yield {pubkey: e.pubkey, server: e.server}
973 yield {pubkey: e.pubkey, server: e.server}
975 let merged = mergeByPubkeyServer presence trustedDeduped
974 let merged = mergeByPubkeyServer presence trustedDeduped
976 let offlineOnlyKeys = do
975 let offlineOnlyKeys = do
977− k <- affectedKeys
976+ e <- allValidEntries
978− where none trustedDeduped (\d -> samePubkeyServer d k)
977+ where none trustedDeduped (\d -> samePubkeyServer d e)
979− yield k
978+ yield e
980 let appliedOffline = do
979 let appliedOffline = do
981 k <- offlineOnlyKeys
980 k <- offlineOnlyKeys
982 where any presence (\p -> samePubkeyServer p k && any trustedOffline (\o -> samePubkeyServer o k && (if o.timestamp > t then t else o.timestamp) > p.timestamp))
981 where any presence (\p -> samePubkeyServer p k && any trustedOffline (\o -> samePubkeyServer o k && (if o.timestamp > t then t else o.timestamp) > p.timestamp))
@@ -1000,10 +999,6 @@ serve = \req -> case req of
1000 set *presence = finalPresence
999 set *presence = finalPresence
1001 let gossipPeerServers = collectPeerServers finalPresence (\p -> p.server)
1000 let gossipPeerServers = collectPeerServers finalPresence (\p -> p.server)
1002 yield {appliedKeys: union capResult.appliedOnline appliedOffline, expiredLocal: pr.expiredLocal, peerServers: pr.peerServers, gossipPeerServers: gossipPeerServers, withinCapacity: withinCapacity}
1001 yield {appliedKeys: union capResult.appliedOnline appliedOffline, expiredLocal: pr.expiredLocal, peerServers: pr.peerServers, gossipPeerServers: gossipPeerServers, withinCapacity: withinCapacity}
1003− let filteredOffline = do
1004− e <- offlineEntries
1005− where none onlineEntries (\o -> samePubkeyServer o e && o.timestamp > e.timestamp)
1006− yield e
1007 let effectiveEvents = do
1002 let effectiveEvents = do
1008 e <- union filteredOnline filteredOffline
1003 e <- union filteredOnline filteredOffline
1009 where any gossipResult.appliedKeys (\a -> samePubkeyServer a e)
1004 where any gossipResult.appliedKeys (\a -> samePubkeyServer a e)