@@ -126,7 +126,8 @@ module App =
126 Error = None
126 Error = None
127 PollCursor = 0L
127 PollCursor = 0L
128 Profile = None
128 Profile = None
129− FlushingOutbox = false },
129+ FlushingOutbox = false
130 [ CmdLoadState ]
131 [ CmdLoadState ]
132 // ── Update ──
133 // ── Update ──
@@ -167,29 +168,32 @@ module App =
167 Error = None
168 Error = None
168 PollStatus = "polling..."
169 PollStatus = "polling..."
169 FlushingOutbox = true },
170 FlushingOutbox = true },
170− [ CmdPoll(session, model.PollCursor); CmdFlushOutbox session ]
171+ [ CmdPoll(session, model.PollCursor, 0); CmdFlushOutbox session ]
171 | _ -> model, []
172 | _ -> model, []
173 | AuthErr err ->
174 | AuthErr err ->
174 { setConn Offline model with
175 { setConn Offline model with
175 Error = Some err
176 Error = Some err
176− FlushingOutbox = false },
177+ FlushingOutbox = false
179 | DoDisconnect ->
181 | DoDisconnect ->
180 { setConn Offline model with
182 { setConn Offline model with
181 PollStatus = ""
183 PollStatus = ""
182− FlushingOutbox = false },
184+ FlushingOutbox = false
185 | DoSend ->
188 | DoSend ->
186 match model.Page with
189 match model.Page with
187− | Chat(pk, compose) when compose <> "" ->
190+ | Chat(pk, compose) when compose.Trim() <> "" ->
191+ let body = compose.Trim()
188 let id = Guid.NewGuid().ToString()
192 let id = Guid.NewGuid().ToString()
190 let outMsg =
194 let outMsg =
191 { Id = id
195 { Id = id
192− Body = compose
196+ Body = body
193 Timestamp = DateTimeOffset.UtcNow
197 Timestamp = DateTimeOffset.UtcNow
194 Direction = Outgoing DeliveryStatus.Sent }
198 Direction = Outgoing DeliveryStatus.Sent }
@@ -201,7 +205,7 @@ module App =
201 Messages = model.Messages |> appendMessage pk outMsg }
205 Messages = model.Messages |> appendMessage pk outMsg }
204− [ CmdEnqueue(pk, Envelope.TextMessage(id, compose))
208+ [ CmdEnqueue(pk, Envelope.TextMessage(id, body))
205 match model.Profile with
209 match model.Profile with
206 | Some profile when isFirstInteraction -> CmdEnqueue(pk, Envelope.ProfileMessage profile)
210 | Some profile when isFirstInteraction -> CmdEnqueue(pk, Envelope.ProfileMessage profile)
207 | _ -> ()
211 | _ -> ()
@@ -225,17 +229,24 @@ module App =
225 { model with FlushingOutbox = false }, []
229 { model with FlushingOutbox = false }, []
227 | PollResult(events, status, newCursor) ->
231 | PollResult(events, status, newCursor) ->
233+ if events.IsEmpty && status.StartsWith("poll error") then
234+ model.PollRetries + 1
228 let model' =
238 let model' =
230 |> List.fold
240 |> List.fold
231 applyEvent
241 applyEvent
232 { model with
242 { model with
233 PollStatus = status
243 PollStatus = status
234− PollCursor = newCursor }
244+ PollCursor = newCursor
245+ PollRetries = retries }
237 [ match trySession model' with
248 [ match trySession model' with
238− | Some session -> CmdPoll(session, model'.PollCursor)
249+ | Some session -> CmdPoll(session, model'.PollCursor, model'.PollRetries)
239 | None -> ()
250 | None -> ()
240 if not events.IsEmpty then
251 if not events.IsEmpty then
241 saveCmdMsg model' ]
252 saveCmdMsg model' ]
@@ -443,25 +454,36 @@ module App =
443 match! Store.peekOutbox () with
454 match! Store.peekOutbox () with
444 | Some item ->
455 | Some item ->
446− let blob =
457+ let blobOpt =
447 Crypto.encrypt
458 Crypto.encrypt
448 session.Identity.PrivKey
459 session.Identity.PrivKey
449 (Crypto.fromHex item.RecipientHex)
460 (Crypto.fromHex item.RecipientHex)
450 item.EnvelopeJson
461 item.EnvelopeJson
452− let ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
465+ let ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
453− do! sendMessage session.Url session.Token item.RecipientHex (Crypto.toHex blob) ts
466+ do! sendMessage session.Url session.Token item.RecipientHex (Crypto.toHex blob) ts
467+ do! Store.dequeueOutbox item.Id
468+ dispatch (FlushResult true)
470+ log $"outbox encrypt failed, dropping message to {item.RecipientHex}"
471+ do! Store.dequeueOutbox item.Id
472+ dispatch (FlushResult true)
474+ | ServerRejected msg ->
475+ log $"outbox rejected, dropping: {msg}"
454 do! Store.dequeueOutbox item.Id
476 do! Store.dequeueOutbox item.Id
455 dispatch (FlushResult true)
477 dispatch (FlushResult true)
456− with ex ->
478+ | ex ->
457 log $"outbox send error: {ex.Message}"
479 log $"outbox send error: {ex.Message}"
458− do! Async.Sleep 3000
480+ do! Async.Sleep Constants.outboxRetryDelayMs
459 dispatch (FlushResult true)
481 dispatch (FlushResult true)
460 | None -> dispatch (FlushResult false)
482 | None -> dispatch (FlushResult false)
462 |> Async.Start)
484 |> Async.Start)
464− | CmdPoll(session, cursor) ->
486+ | CmdPoll(session, cursor, retries) ->
465 Cmd.ofEffect (fun dispatch ->
487 Cmd.ofEffect (fun dispatch ->
467 log $"poll start cursor={cursor}"
489 log $"poll start cursor={cursor}"
@@ -493,33 +515,30 @@ module App =
493 for e in errors do
515 for e in errors do
494 log $"decrypt error: {e}"
516 log $"decrypt error: {e}"
496− if not ackIds.IsEmpty || not events.IsEmpty then
518+ if not ackIds.IsEmpty then
498− if not ackIds.IsEmpty then
500− do! ackMessages session.Url session.Token ackIds
520+ do! ackMessages session.Url session.Token ackIds
501− with ex ->
521+ with ex ->
502− log $"ack error: {ex.Message}"
522+ log $"ack error: {ex.Message}"
524+ if not events.IsEmpty then
504− let acksBySender =
525+ let acksBySender =
506− |> List.choose (fun evt ->
527+ |> List.choose (fun evt ->
507− match evt.Envelope with
528+ match evt.Envelope with
508− | Envelope.TextMessage(id, _) -> Some(evt.Sender, id)
529+ | Envelope.TextMessage(id, _) -> Some(evt.Sender, id)
509− | _ -> None)
530+ | _ -> None)
510− |> List.groupBy fst
531+ |> List.groupBy fst
511− |> List.map (fun (sender, pairs) -> sender, List.map snd pairs)
532+ |> List.map (fun (sender, pairs) -> sender, List.map snd pairs)
513− for sender, msgIds in acksBySender do
534+ for sender, msgIds in acksBySender do
515− do! sendEnvelope session sender (Envelope.DeliveryAck msgIds)
536+ do! sendEnvelope session sender (Envelope.DeliveryAck msgIds)
516− with ex ->
537+ with ex ->
517− log $"delivery ack error: {ex.Message}"
538+ log $"delivery ack error: {ex.Message}"
521 if response.Events.Length = 0 then
540 if response.Events.Length = 0 then
522− do! Async.Sleep 5000
541+ do! Async.Sleep Constants.pollEmptyDelayMs
524 let chatCount =
543 let chatCount =
@@ -541,7 +560,8 @@ module App =
541 dispatch (PollResult([], "polling...", cursor))
560 dispatch (PollResult([], "polling...", cursor))
543 log $"poll error: {ex.Message}"
562 log $"poll error: {ex.Message}"
563+ let delay = min (Constants.pollRetryBaseMs * (pown 2 retries)) 30000
544− do! Async.Sleep 3000
564+ do! Async.Sleep delay
545 dispatch (PollResult([], $"poll error: {ex.Message}", cursor))
565 dispatch (PollResult([], $"poll error: {ex.Message}", cursor))
547 |> Async.Start)
567 |> Async.Start)