on master help sign in
Migrate to Donald for data access and fix profile message propagation for unknown senders
d9813d4iko
client/App.fs+7−1
@@ -89,7 +89,11 @@ module App =
89 Messages = m.Messages |> markDelivered evt.Sender ackIds }
89 Messages = m.Messages |> markDelivered evt.Sender ackIds }
90 | Envelope.ProfileMessage profile ->
90 | Envelope.ProfileMessage profile ->
91 { m with
91 { m with
92+ Contacts =
93+ m.Contacts
92− Contacts = m.Contacts |> Map.change evt.Sender (Option.map (withProfile profile)) }
94+ |> Map.change evt.Sender (fun c ->
95+ let contact = c |> Option.defaultWith (fun () -> newContact evt.Sender "")
96+ Some(withProfile profile contact)) }
93
97
94 let private trySession model =
98 let private trySession model =
95 match model.Auth with
99 match model.Auth with
@@ -336,6 +340,8 @@ module App =
336 []
340 []
337 | StateLoaded(None, _, _) -> model, []
341 | StateLoaded(None, _, _) -> model, []
338
342
343+ | Search _ -> model, []
344+
339 | KeyboardHeightChanged h -> { model with KeyboardHeight = h }, []
345 | KeyboardHeightChanged h -> { model with KeyboardHeight = h }, []
340
346
341 // ── mapCmd ──
347 // ── mapCmd ──
client/Skrepka.fsproj+1−1
@@ -125,7 +125,7 @@
125 <PackageReference Include="Fabulous" Version="3.0.0-pre22" />
125 <PackageReference Include="Fabulous" Version="3.0.0-pre22" />
126 <PackageReference Include="Fabulous.Avalonia" Version="3.0.0-pre13" />
126 <PackageReference Include="Fabulous.Avalonia" Version="3.0.0-pre13" />
127 <PackageReference Include="Sodium.Core" Version="1.4.0" />
127 <PackageReference Include="Sodium.Core" Version="1.4.0" />
128− <PackageReference Include="Dapper.FSharp" Version="4.10.0" />
128+ <PackageReference Include="Donald" Version="10.0.2" />
129 <PackageReference Include="Microsoft.Data.Sqlite" Version="11.0.0-preview.2.26159.112" />
129 <PackageReference Include="Microsoft.Data.Sqlite" Version="11.0.0-preview.2.26159.112" />
130 <PackageReference Include="QRCoder" Version="1.6.0" />
130 <PackageReference Include="QRCoder" Version="1.6.0" />
131 </ItemGroup>
131 </ItemGroup>
client/Store.fs+159−261
@@ -1,10 +1,10 @@
1namespace Skrepka
1namespace Skrepka
2
2
3open System
3open System
4+open System.Data
4open System.IO
5open System.IO
5−open Dapper
6+open Donald
6open Microsoft.Data.Sqlite
7open Microsoft.Data.Sqlite
7−open Dapper.FSharp.SQLite
8
8
9module Store =
9module Store =
10
10
@@ -41,50 +41,6 @@ module Store =
41 ServerUrl: string
41 ServerUrl: string
42 PollCursor: int64 }
42 PollCursor: int64 }
43
43
44− // DB row types matching table columns
45− [<CLIMutable>]
46− type ProfileRow =
47− { Id: string
48− DisplayName: string
49− Bio: string
50− Photo: byte[] option }
51−
52− [<CLIMutable>]
53− type ContactRow =
54− { Pubkey: string
55− Nickname: string
56− DisplayName: string
57− Bio: string
58− Photo: byte[] option }
59−
60− [<CLIMutable>]
61− type MessageRow =
62− { Id: string
63− ConversationId: string
64− Body: string
65− TimestampUnix: int64
66− IsOutgoing: bool
67− Status: int }
68−
69− [<CLIMutable>]
70− type SettingsRow =
71− { Id: string
72− ServerUrl: string
73− PollCursor: int64 }
74−
75− [<CLIMutable>]
76− type OutboxRow =
77− { Id: int64
78− RecipientHex: string
79− EnvelopeJson: string
80− CreatedAt: int64 }
81−
82− let private profileTable = table'<ProfileRow> "profile"
83− let private contactTable = table'<ContactRow> "contacts"
84− let private messageTable = table'<MessageRow> "messages"
85− let private settingsTable = table'<SettingsRow> "settings"
86− let private outboxTable = table'<OutboxRow> "outbox"
87−
88 let private log msg = eprintfn $"[skrepka] {msg}"
44 let private log msg = eprintfn $"[skrepka] {msg}"
89
45
90 let private appDataDir =
46 let private appDataDir =
@@ -109,78 +65,87 @@ module Store =
109 let private openConn () =
65 let private openConn () =
110 let conn = new SqliteConnection($"Data Source={dbPath}")
66 let conn = new SqliteConnection($"Data Source={dbPath}")
111 conn.Open()
67 conn.Open()
112− conn.Execute("PRAGMA busy_timeout = 5000") |> ignore
68+ conn |> Db.newCommand "PRAGMA busy_timeout = 5000" |> Db.exec
113 conn
69 conn
114
70
115− type private ByteArrayOptionHandler() =
71+ // ── Readers ──
116− inherit SqlMapper.TypeHandler<byte[] option>()
72+
117−
73+ let private readContact (rd: IDataReader) : Contact =
118− override _.SetValue(p, value) =
74+ { Pubkey = rd.ReadString "Pubkey"
119− p.Value <-
75+ Nickname = rd.ReadString "Nickname"
120− match value with
76+ DisplayName = rd.ReadString "DisplayName"
121− | Some v -> box v
77+ Bio = rd.ReadString "Bio"
122− | None -> box DBNull.Value
78+ Photo = rd.ReadBytesOption "Photo" }
123−
79+
124− override _.Parse(value) =
80+ let private readMessage (rd: IDataReader) : ChatMessage =
125− if value = null || value :? DBNull then
81+ let isOutgoing = rd.ReadBoolean "IsOutgoing"
126− None
82+ let status = rd.ReadInt32 "Status"
83+
84+ { Id = rd.ReadString "Id"
85+ Body = rd.ReadString "Body"
86+ Timestamp = rd.ReadInt64 "TimestampUnix" |> DateTimeOffset.FromUnixTimeMilliseconds
87+ Direction =
88+ if not isOutgoing then
89+ Incoming
127 else
90 else
128− Some(value :?> byte[])
91+ Outgoing(if status = 1 then DeliveryStatus.Delivered else DeliveryStatus.Sent) }
92+
93+ // ── Schema ──
129
94
130 do
95 do
131− OptionTypes.register ()
132− SqlMapper.AddTypeHandler(ByteArrayOptionHandler())
133 use conn = openConn ()
96 use conn = openConn ()
134− conn.Execute("PRAGMA journal_mode=WAL") |> ignore
97+ conn |> Db.newCommand "PRAGMA journal_mode=WAL" |> Db.exec
135
98
136− conn.Execute(
99+ conn
100+ |> Db.newCommand
137 "CREATE TABLE IF NOT EXISTS profile (Id TEXT PRIMARY KEY, DisplayName TEXT NOT NULL DEFAULT '', Bio TEXT NOT NULL DEFAULT '', Photo BLOB)"
101 "CREATE TABLE IF NOT EXISTS profile (Id TEXT PRIMARY KEY, DisplayName TEXT NOT NULL DEFAULT '', Bio TEXT NOT NULL DEFAULT '', Photo BLOB)"
138− )
102+ |> Db.exec
139− |> ignore
140
103
141− conn.Execute(
104+ conn
105+ |> Db.newCommand
142 "CREATE TABLE IF NOT EXISTS contacts (Pubkey TEXT PRIMARY KEY, Nickname TEXT NOT NULL DEFAULT '', DisplayName TEXT NOT NULL DEFAULT '', Bio TEXT NOT NULL DEFAULT '', Photo BLOB)"
106 "CREATE TABLE IF NOT EXISTS contacts (Pubkey TEXT PRIMARY KEY, Nickname TEXT NOT NULL DEFAULT '', DisplayName TEXT NOT NULL DEFAULT '', Bio TEXT NOT NULL DEFAULT '', Photo BLOB)"
143− )
107+ |> Db.exec
144− |> ignore
145
108
146− conn.Execute(
109+ conn
110+ |> Db.newCommand
147 "CREATE TABLE IF NOT EXISTS messages (Id TEXT PRIMARY KEY, ConversationId TEXT NOT NULL, Body TEXT NOT NULL DEFAULT '', TimestampUnix INTEGER NOT NULL, IsOutgoing INTEGER NOT NULL DEFAULT 0, Status INTEGER NOT NULL DEFAULT 0)"
111 "CREATE TABLE IF NOT EXISTS messages (Id TEXT PRIMARY KEY, ConversationId TEXT NOT NULL, Body TEXT NOT NULL DEFAULT '', TimestampUnix INTEGER NOT NULL, IsOutgoing INTEGER NOT NULL DEFAULT 0, Status INTEGER NOT NULL DEFAULT 0)"
148− )
112+ |> Db.exec
149− |> ignore
150
113
151− conn.Execute(
114+ conn
115+ |> Db.newCommand
152 "CREATE TABLE IF NOT EXISTS settings (Id TEXT PRIMARY KEY, ServerUrl TEXT NOT NULL DEFAULT '', PollCursor INTEGER NOT NULL DEFAULT 0)"
116 "CREATE TABLE IF NOT EXISTS settings (Id TEXT PRIMARY KEY, ServerUrl TEXT NOT NULL DEFAULT '', PollCursor INTEGER NOT NULL DEFAULT 0)"
153− )
117+ |> Db.exec
154− |> ignore
155
118
156− conn.Execute(
119+ conn
120+ |> Db.newCommand
157 "CREATE TABLE IF NOT EXISTS outbox (Id INTEGER PRIMARY KEY AUTOINCREMENT, RecipientHex TEXT NOT NULL, EnvelopeJson TEXT NOT NULL, CreatedAt INTEGER NOT NULL)"
121 "CREATE TABLE IF NOT EXISTS outbox (Id INTEGER PRIMARY KEY AUTOINCREMENT, RecipientHex TEXT NOT NULL, EnvelopeJson TEXT NOT NULL, CreatedAt INTEGER NOT NULL)"
158− )
122+ |> Db.exec
159− |> ignore
160
123
161− // Schema versioning
124+ conn
162− conn.Execute("CREATE TABLE IF NOT EXISTS schema_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)")
125+ |> Db.newCommand "CREATE TABLE IF NOT EXISTS schema_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)"
163− |> ignore
126+ |> Db.exec
164
127
165 let dbVersion =
128 let dbVersion =
166 try
129 try
130+ conn
167− conn.QueryFirstOrDefault<string>("SELECT value FROM schema_meta WHERE key = 'schema_version'")
131+ |> Db.newCommand "SELECT value FROM schema_meta WHERE key = 'schema_version'"
168− |> Option.ofObj
132+ |> Db.querySingle (fun rd -> rd.ReadString "value" |> int)
169− |> Option.map int
170 |> Option.defaultValue 0
133 |> Option.defaultValue 0
171 with ex ->
134 with ex ->
172 log $"schema version check: {ex.Message}"
135 log $"schema version check: {ex.Message}"
173 0
136 0
174
137
175 if dbVersion < 2 then
138 if dbVersion < 2 then
139+ conn
176− conn.Execute("CREATE INDEX IF NOT EXISTS idx_messages_conv ON messages(ConversationId)")
140+ |> Db.newCommand "CREATE INDEX IF NOT EXISTS idx_messages_conv ON messages(ConversationId)"
177− |> ignore
141+ |> Db.exec
178
142
179− conn.Execute(
143+ conn
180− "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', @v)",
144+ |> Db.newCommand "INSERT OR REPLACE INTO schema_meta (key, value) VALUES (@key, @value)"
181− {| v = string Constants.schemaVersion |}
145+ |> Db.setParams [ "key", SqlType.String "schema_version"; "value", SqlType.String(string Constants.schemaVersion) ]
182− )
146+ |> Db.exec
183− |> ignore
147+
148+ // ── Identity ──
184
149
185 let private identityPath = Path.Combine(appDataDir, "identity.key")
150 let private identityPath = Path.Combine(appDataDir, "identity.key")
186
151
@@ -206,26 +171,21 @@ module Store =
206 File.WriteAllText(identityPath, b64)
171 File.WriteAllText(identityPath, b64)
207 }
172 }
208
173
174+ // ── Profile ──
175+
209 let loadProfile () =
176 let loadProfile () =
210 async {
177 async {
211 try
178 try
212 use conn = openConn ()
179 use conn = openConn ()
213
180
214− let! rows =
215− select {
216− for p in profileTable do
217− where (p.Id = Constants.profileId)
218− }
219− |> conn.SelectAsync<ProfileRow>
220− |> Async.AwaitTask
221−
222 return
181 return
223− rows
182+ conn
224− |> Seq.tryHead
183+ |> Db.newCommand "SELECT DisplayName, Bio, Photo FROM profile WHERE Id = @Id"
225− |> Option.map (fun p ->
184+ |> Db.setParams [ "Id", SqlType.String Constants.profileId ]
185+ |> Db.querySingle (fun rd ->
226− { DisplayName = p.DisplayName
186+ { DisplayName = rd.ReadString "DisplayName"
227− Bio = p.Bio
187+ Bio = rd.ReadString "Bio"
228− Photo = p.Photo })
188+ Photo = rd.ReadBytesOption "Photo" })
229 with ex ->
189 with ex ->
230 log $"loadProfile error: {ex.Message}"
190 log $"loadProfile error: {ex.Message}"
231 return None
191 return None
@@ -236,93 +196,60 @@ module Store =
236 try
196 try
237 use conn = openConn ()
197 use conn = openConn ()
238
198
239− do!
240− conn.ExecuteAsync(
199+ conn
200+ |> Db.newCommand
241− "INSERT OR REPLACE INTO profile (Id, DisplayName, Bio, Photo) VALUES (@Id, @DisplayName, @Bio, @Photo)",
201+ "INSERT OR REPLACE INTO profile (Id, DisplayName, Bio, Photo) VALUES (@Id, @DisplayName, @Bio, @Photo)"
202+ |> Db.setParams
242− {| Id = Constants.profileId
203+ [ "Id", SqlType.String Constants.profileId
243− DisplayName = profile.DisplayName
204+ "DisplayName", SqlType.String profile.DisplayName
244− Bio = profile.Bio
205+ "Bio", SqlType.String profile.Bio
206+ "Photo",
245− Photo = profile.Photo |> Option.defaultValue null |}
207+ (match profile.Photo with
246− )
208+ | Some bytes -> SqlType.Bytes bytes
247− |> Async.AwaitTask
209+ | None -> SqlType.Null) ]
248− |> Async.Ignore
210+ |> Db.exec
249 with ex ->
211 with ex ->
250 log $"saveProfile error: {ex.Message}"
212 log $"saveProfile error: {ex.Message}"
251 }
213 }
252
214
215+ // ── Data ──
216+
253 let loadData () =
217 let loadData () =
254 async {
218 async {
255 try
219 try
256 use conn = openConn ()
220 use conn = openConn ()
257
221
258− let! settings =
222+ let settings =
259− select {
223+ conn
260− for s in settingsTable do
224+ |> Db.newCommand "SELECT ServerUrl, PollCursor FROM settings WHERE Id = @Id"
261− where (s.Id = Constants.settingsId)
225+ |> Db.setParams [ "Id", SqlType.String Constants.settingsId ]
262− }
226+ |> Db.querySingle (fun rd -> rd.ReadString "ServerUrl", rd.ReadInt64 "PollCursor")
263− |> conn.SelectAsync<SettingsRow>
264− |> Async.AwaitTask
265
227
266− match settings |> Seq.tryHead with
228+ match settings with
267 | None -> return None
229 | None -> return None
268− | Some s ->
230+ | Some(serverUrl, pollCursor) ->
269− let! contactRows =
270− select {
271− for c in contactTable do
272− selectAll
273− }
274− |> conn.SelectAsync<ContactRow>
275− |> Async.AwaitTask
276−
277 let contacts =
231 let contacts =
278− contactRows
232+ conn
233+ |> Db.newCommand "SELECT Pubkey, Nickname, DisplayName, Bio, Photo FROM contacts"
234+ |> Db.query readContact
279− |> Seq.map (fun c ->
235+ |> List.map (fun c -> c.Pubkey, c)
280− let contact: Contact =
236+ |> Map.ofList
281− { Pubkey = c.Pubkey
282− Nickname = c.Nickname
283− DisplayName = c.DisplayName
284− Bio = c.Bio
285− Photo = c.Photo }
286−
287− c.Pubkey, contact)
288− |> Map.ofSeq
289−
290− let! messageRows =
291− conn.QueryAsync<MessageRow>(
292− $"SELECT Id, ConversationId, Body, TimestampUnix, IsOutgoing, Status FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY ConversationId ORDER BY TimestampUnix DESC) AS rn FROM messages) WHERE rn <= {Constants.maxMessages}"
293− )
294− |> Async.AwaitTask
295
237
296 let messages =
238 let messages =
297− messageRows
239+ conn
298− |> Seq.groupBy _.ConversationId
240+ |> Db.newCommand
299− |> Seq.map (fun (convId, rows) ->
241+ $"SELECT Id, Body, TimestampUnix, IsOutgoing, Status, ConversationId FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY ConversationId ORDER BY TimestampUnix DESC) AS rn FROM messages) WHERE rn <= {Constants.maxMessages}"
300− convId,
242+ |> Db.query (fun rd -> rd.ReadString "ConversationId", readMessage rd)
301− rows
243+ |> List.groupBy fst
302− |> Seq.map (fun m ->
244+ |> List.map (fun (convId, pairs) -> convId, pairs |> List.map snd |> List.sortBy _.Timestamp)
303− { Id = m.Id
245+ |> Map.ofList
304− Body = m.Body
305− Timestamp = DateTimeOffset.FromUnixTimeMilliseconds m.TimestampUnix
306− Direction =
307− if not m.IsOutgoing then
308− Incoming
309− else
310− Outgoing(
311− if m.Status = 1 then
312− DeliveryStatus.Delivered
313− else
314− DeliveryStatus.Sent
315− ) })
316− |> Seq.sortBy _.Timestamp
317− |> Seq.toList)
318− |> Map.ofSeq
319
246
320 return
247 return
321 Some
248 Some
322 { Contacts = contacts
249 { Contacts = contacts
323 Messages = messages
250 Messages = messages
324− ServerUrl = s.ServerUrl
251+ ServerUrl = serverUrl
325− PollCursor = s.PollCursor }
252+ PollCursor = pollCursor }
326 with ex ->
253 with ex ->
327 log $"loadData error: {ex.Message}"
254 log $"loadData error: {ex.Message}"
328 return None
255 return None
@@ -335,18 +262,19 @@ module Store =
335 use tx = conn.BeginTransaction()
262 use tx = conn.BeginTransaction()
336
263
337 for c in data.Contacts.Values do
264 for c in data.Contacts.Values do
338− do!
339− conn.ExecuteAsync(
265+ conn
266+ |> Db.newCommand
340− "INSERT OR REPLACE INTO contacts (Pubkey, Nickname, DisplayName, Bio, Photo) VALUES (@Pubkey, @Nickname, @DisplayName, @Bio, @Photo)",
267+ "INSERT OR REPLACE INTO contacts (Pubkey, Nickname, DisplayName, Bio, Photo) VALUES (@Pubkey, @Nickname, @DisplayName, @Bio, @Photo)"
268+ |> Db.setParams
341− {| Pubkey = c.Pubkey
269+ [ "Pubkey", SqlType.String c.Pubkey
342− Nickname = c.Nickname
270+ "Nickname", SqlType.String c.Nickname
343− DisplayName = c.DisplayName
271+ "DisplayName", SqlType.String c.DisplayName
344− Bio = c.Bio
272+ "Bio", SqlType.String c.Bio
345− Photo = c.Photo |},
273+ "Photo",
346− tx
274+ (match c.Photo with
347− )
275+ | Some bytes -> SqlType.Bytes bytes
348− |> Async.AwaitTask
276+ | None -> SqlType.Null) ]
349− |> Async.Ignore
277+ |> Db.exec
350
278
351 for KeyValue(convId, msgs) in data.Messages do
279 for KeyValue(convId, msgs) in data.Messages do
352 for m in msgs do
280 for m in msgs do
@@ -356,55 +284,47 @@ module Store =
356 | Outgoing DeliveryStatus.Delivered -> true, 1
284 | Outgoing DeliveryStatus.Delivered -> true, 1
357 | Outgoing DeliveryStatus.Sent -> true, 0
285 | Outgoing DeliveryStatus.Sent -> true, 0
358
286
359− do!
287+ conn
360− conn.ExecuteAsync(
288+ |> Db.newCommand
361− "INSERT OR REPLACE INTO messages (Id, ConversationId, Body, TimestampUnix, IsOutgoing, Status) VALUES (@Id, @ConversationId, @Body, @TimestampUnix, @IsOutgoing, @Status)",
289+ "INSERT OR REPLACE INTO messages (Id, ConversationId, Body, TimestampUnix, IsOutgoing, Status) VALUES (@Id, @ConversationId, @Body, @TimestampUnix, @IsOutgoing, @Status)"
290+ |> Db.setParams
362− {| Id = m.Id
291+ [ "Id", SqlType.String m.Id
363− ConversationId = convId
292+ "ConversationId", SqlType.String convId
364− Body = m.Body
293+ "Body", SqlType.String m.Body
365− TimestampUnix = m.Timestamp.ToUnixTimeMilliseconds()
294+ "TimestampUnix", SqlType.Int64(m.Timestamp.ToUnixTimeMilliseconds())
366− IsOutgoing = isOutgoing
295+ "IsOutgoing", SqlType.Boolean isOutgoing
367− Status = status |},
296+ "Status", SqlType.Int32 status ]
368− tx
297+ |> Db.exec
369− )
298+
370− |> Async.AwaitTask
299+ conn
371− |> Async.Ignore
300+ |> Db.newCommand
372−
373− do!
374− conn.ExecuteAsync(
375− "INSERT OR REPLACE INTO settings (Id, ServerUrl, PollCursor) VALUES (@Id, @ServerUrl, @PollCursor)",
301+ "INSERT OR REPLACE INTO settings (Id, ServerUrl, PollCursor) VALUES (@Id, @ServerUrl, @PollCursor)"
302+ |> Db.setParams
376− {| Id = Constants.settingsId
303+ [ "Id", SqlType.String Constants.settingsId
377− ServerUrl = data.ServerUrl
304+ "ServerUrl", SqlType.String data.ServerUrl
378− PollCursor = data.PollCursor |},
305+ "PollCursor", SqlType.Int64 data.PollCursor ]
379− tx
306+ |> Db.exec
380− )
381− |> Async.AwaitTask
382− |> Async.Ignore
383
307
384 tx.Commit()
308 tx.Commit()
385 with ex ->
309 with ex ->
386 log $"saveData error: {ex.Message}"
310 log $"saveData error: {ex.Message}"
387 }
311 }
388
312
313+ // ── Outbox ──
314+
389 let enqueueOutbox (recipientHex: string) (envelopeJson: string) =
315 let enqueueOutbox (recipientHex: string) (envelopeJson: string) =
390 async {
316 async {
391 try
317 try
392 use conn = openConn ()
318 use conn = openConn ()
393
319
394− do!
320+ conn
395− insert {
321+ |> Db.newCommand
396− for o in outboxTable do
322+ "INSERT INTO outbox (RecipientHex, EnvelopeJson, CreatedAt) VALUES (@RecipientHex, @EnvelopeJson, @CreatedAt)"
397− value
323+ |> Db.setParams
398− { Id = 0L
399− RecipientHex = recipientHex
324+ [ "RecipientHex", SqlType.String recipientHex
400− EnvelopeJson = envelopeJson
325+ "EnvelopeJson", SqlType.String envelopeJson
401− CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }
326+ "CreatedAt", SqlType.Int64(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()) ]
402−
327+ |> Db.exec
403− excludeColumn o.Id
404− }
405− |> conn.InsertAsync
406− |> Async.AwaitTask
407− |> Async.Ignore
408 with ex ->
328 with ex ->
409 log $"enqueueOutbox error: {ex.Message}"
329 log $"enqueueOutbox error: {ex.Message}"
410 }
330 }
@@ -414,13 +334,13 @@ module Store =
414 try
334 try
415 use conn = openConn ()
335 use conn = openConn ()
416
336
417− let! rows =
337+ return
418− conn.QueryAsync<OutboxRow>(
338+ conn
419− "SELECT Id, RecipientHex, EnvelopeJson, CreatedAt FROM outbox ORDER BY Id LIMIT 1"
339+ |> Db.newCommand "SELECT Id, RecipientHex, EnvelopeJson, CreatedAt FROM outbox ORDER BY Id LIMIT 1"
420− )
340+ |> Db.querySingle (fun rd ->
421− |> Async.AwaitTask
341+ {| Id = rd.ReadInt64 "Id"
422−
342+ RecipientHex = rd.ReadString "RecipientHex"
423− return rows |> Seq.tryHead
343+ EnvelopeJson = rd.ReadString "EnvelopeJson" |})
424 with ex ->
344 with ex ->
425 log $"peekOutbox error: {ex.Message}"
345 log $"peekOutbox error: {ex.Message}"
426 return None
346 return None
@@ -431,14 +351,10 @@ module Store =
431 try
351 try
432 use conn = openConn ()
352 use conn = openConn ()
433
353
434− do!
354+ conn
435− delete {
355+ |> Db.newCommand "DELETE FROM outbox WHERE Id = @Id"
436− for o in outboxTable do
356+ |> Db.setParams [ "Id", SqlType.Int64 id ]
437− where (o.Id = id)
357+ |> Db.exec
438− }
439− |> conn.DeleteAsync
440− |> Async.AwaitTask
441− |> Async.Ignore
442 with ex ->
358 with ex ->
443 log $"dequeueOutbox error: {ex.Message}"
359 log $"dequeueOutbox error: {ex.Message}"
444 }
360 }
@@ -448,30 +364,12 @@ module Store =
448 try
364 try
449 use conn = openConn ()
365 use conn = openConn ()
450
366
451− let! rows =
452− conn.QueryAsync<MessageRow>(
453− "SELECT Id, ConversationId, Body, TimestampUnix, IsOutgoing, Status FROM messages WHERE ConversationId = @convId ORDER BY TimestampUnix",
454− {| convId = conversationId |}
455− )
456− |> Async.AwaitTask
457−
458 return
367 return
459− rows
368+ conn
460− |> Seq.map (fun m ->
369+ |> Db.newCommand
461− { Id = m.Id
370+ "SELECT Id, Body, TimestampUnix, IsOutgoing, Status FROM messages WHERE ConversationId = @convId ORDER BY TimestampUnix"
462− Body = m.Body
371+ |> Db.setParams [ "convId", SqlType.String conversationId ]
463− Timestamp = DateTimeOffset.FromUnixTimeMilliseconds m.TimestampUnix
372+ |> Db.query readMessage
464− Direction =
465− if not m.IsOutgoing then
466− Incoming
467− else
468− Outgoing(
469− if m.Status = 1 then
470− DeliveryStatus.Delivered
471− else
472− DeliveryStatus.Sent
473− ) })
474− |> Seq.toList
475 with ex ->
373 with ex ->
476 log $"loadMessagesForConversation error: {ex.Message}"
374 log $"loadMessagesForConversation error: {ex.Message}"
477 return []
375 return []