@@ -126,7 +126,8 @@ module App =
126126 Error = None
127127 PollCursor = 0L
128128 Profile = None
129− FlushingOutbox = false },
129+ FlushingOutbox = false
130+ PollRetries = 0 },
130131 [ CmdLoadState ]
131132
132133 // ── Update ──
@@ -167,29 +168,32 @@ module App =
167168 Error = None
168169 PollStatus = "polling..."
169170 FlushingOutbox = true },
170− [ CmdPoll(session, model.PollCursor); CmdFlushOutbox session ]
171+ [ CmdPoll(session, model.PollCursor, 0); CmdFlushOutbox session ]
171172 | _ -> model, []
172173
173174 | AuthErr err ->
174175 { setConn Offline model with
175176 Error = Some err
176− FlushingOutbox = false },
177+ FlushingOutbox = false
178+ PollRetries = 0 },
177179 []
178180
179181 | DoDisconnect ->
180182 { setConn Offline model with
181183 PollStatus = ""
182− FlushingOutbox = false },
184+ FlushingOutbox = false
185+ PollRetries = 0 },
183186 []
184187
185188 | DoSend ->
186189 match model.Page with
187− | Chat(pk, compose) when compose <> "" ->
190+ | Chat(pk, compose) when compose.Trim() <> "" ->
191+ let body = compose.Trim()
188192 let id = Guid.NewGuid().ToString()
189193
190194 let outMsg =
191195 { Id = id
192− Body = compose
196+ Body = body
193197 Timestamp = DateTimeOffset.UtcNow
194198 Direction = Outgoing DeliveryStatus.Sent }
195199
@@ -201,7 +205,7 @@ module App =
201205 Messages = model.Messages |> appendMessage pk outMsg }
202206
203207 model',
204− [ CmdEnqueue(pk, Envelope.TextMessage(id, compose))
208+ [ CmdEnqueue(pk, Envelope.TextMessage(id, body))
205209 match model.Profile with
206210 | Some profile when isFirstInteraction -> CmdEnqueue(pk, Envelope.ProfileMessage profile)
207211 | _ -> ()
@@ -225,17 +229,24 @@ module App =
225229 { model with FlushingOutbox = false }, []
226230
227231 | PollResult(events, status, newCursor) ->
232+ let retries =
233+ if events.IsEmpty && status.StartsWith("poll error") then
234+ model.PollRetries + 1
235+ else
236+ 0
237+
228238 let model' =
229239 events
230240 |> List.fold
231241 applyEvent
232242 { model with
233243 PollStatus = status
234− PollCursor = newCursor }
244+ PollCursor = newCursor
245+ PollRetries = retries }
235246
236247 model',
237248 [ match trySession model' with
238− | Some session -> CmdPoll(session, model'.PollCursor)
249+ | Some session -> CmdPoll(session, model'.PollCursor, model'.PollRetries)
239250 | None -> ()
240251 if not events.IsEmpty then
241252 saveCmdMsg model' ]
@@ -443,25 +454,36 @@ module App =
443454 match! Store.peekOutbox () with
444455 | Some item ->
445456 try
446− let blob =
457+ let blobOpt =
447458 Crypto.encrypt
448459 session.Identity.PrivKey
449460 (Crypto.fromHex item.RecipientHex)
450461 item.EnvelopeJson
451462
452− let ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
453− do! sendMessage session.Url session.Token item.RecipientHex (Crypto.toHex blob) ts
463+ match blobOpt with
464+ | Some blob ->
465+ let ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
466+ do! sendMessage session.Url session.Token item.RecipientHex (Crypto.toHex blob) ts
467+ do! Store.dequeueOutbox item.Id
468+ dispatch (FlushResult true)
469+ | None ->
470+ log $"outbox encrypt failed, dropping message to {item.RecipientHex}"
471+ do! Store.dequeueOutbox item.Id
472+ dispatch (FlushResult true)
473+ with
474+ | ServerRejected msg ->
475+ log $"outbox rejected, dropping: {msg}"
454476 do! Store.dequeueOutbox item.Id
455477 dispatch (FlushResult true)
456− with ex ->
478+ | ex ->
457479 log $"outbox send error: {ex.Message}"
458− do! Async.Sleep 3000
480+ do! Async.Sleep Constants.outboxRetryDelayMs
459481 dispatch (FlushResult true)
460482 | None -> dispatch (FlushResult false)
461483 }
462484 |> Async.Start)
463485
464− | CmdPoll(session, cursor) ->
486+ | CmdPoll(session, cursor, retries) ->
465487 Cmd.ofEffect (fun dispatch ->
466488 async {
467489 log $"poll start cursor={cursor}"
@@ -493,33 +515,30 @@ module App =
493515 for e in errors do
494516 log $"decrypt error: {e}"
495517
496− if not ackIds.IsEmpty || not events.IsEmpty then
497− async {
498− if not ackIds.IsEmpty then
499− try
500− do! ackMessages session.Url session.Token ackIds
501− with ex ->
502− log $"ack error: {ex.Message}"
503−
504− let acksBySender =
505− events
506− |> List.choose (fun evt ->
507− match evt.Envelope with
508− | Envelope.TextMessage(id, _) -> Some(evt.Sender, id)
509− | _ -> None)
510− |> List.groupBy fst
511− |> List.map (fun (sender, pairs) -> sender, List.map snd pairs)
512−
513− for sender, msgIds in acksBySender do
514− try
515− do! sendEnvelope session sender (Envelope.DeliveryAck msgIds)
516− with ex ->
517− log $"delivery ack error: {ex.Message}"
518− }
519− |> Async.Start
518+ if not ackIds.IsEmpty then
519+ try
520+ do! ackMessages session.Url session.Token ackIds
521+ with ex ->
522+ log $"ack error: {ex.Message}"
523+
524+ if not events.IsEmpty then
525+ let acksBySender =
526+ events
527+ |> List.choose (fun evt ->
528+ match evt.Envelope with
529+ | Envelope.TextMessage(id, _) -> Some(evt.Sender, id)
530+ | _ -> None)
531+ |> List.groupBy fst
532+ |> List.map (fun (sender, pairs) -> sender, List.map snd pairs)
533+
534+ for sender, msgIds in acksBySender do
535+ try
536+ do! sendEnvelope session sender (Envelope.DeliveryAck msgIds)
537+ with ex ->
538+ log $"delivery ack error: {ex.Message}"
520539
521540 if response.Events.Length = 0 then
522− do! Async.Sleep 5000
541+ do! Async.Sleep Constants.pollEmptyDelayMs
523542
524543 let chatCount =
525544 events
@@ -541,7 +560,8 @@ module App =
541560 dispatch (PollResult([], "polling...", cursor))
542561 | ex ->
543562 log $"poll error: {ex.Message}"
544− do! Async.Sleep 3000
563+ let delay = min (Constants.pollRetryBaseMs * (pown 2 retries)) 30000
564+ do! Async.Sleep delay
545565 dispatch (PollResult([], $"poll error: {ex.Message}", cursor))
546566 }
547567 |> Async.Start)