p2p-swift / Sources / IrdP2PSignaling / Mailbox / MailboxOutbox.swift
388 words
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import Foundation

/// A best-effort retry queue in front of `MailboxClient.deposit`: rate limits,
/// storage pressure and network failures back off (`min(1000·2^n, 300000)` ms),
/// oversize payloads are dropped, auth failures are dropped after the client's
/// own single re-mint. The host app can snapshot and restore the queue.
public actor MailboxOutbox {
    public struct Item: Sendable, Codable, Equatable {
        public var room: String
        public var payload: Data
        public var attempts: Int = 0
        public init(room: String, payload: Data, attempts: Int = 0) {
            self.room = room
            self.payload = payload
            self.attempts = attempts
        }
    }

    public enum Outcome: Sendable, Equatable {
        case deposited(DepositResult)
        case dropped(Item, reason: String)
        case deferred(Item, retryAfter: Duration)
    }

    private let client: MailboxClient
    private let clock: any P2PClock
    private var queue: [Item] = []
    private var flushing = false
    private let onOutcome: (@Sendable (Outcome) -> Void)?

    public init(client: MailboxClient, clock: any P2PClock = SystemClock(), onOutcome: (@Sendable (Outcome) -> Void)? = nil) {
        self.client = client
        self.clock = clock
        self.onOutcome = onOutcome
    }

    public var pending: [Item] { queue }

    public func enqueue(room: String, payload: Data) {
        queue.append(Item(room: room, payload: payload))
    }

    /// Restores a previously snapshotted queue (appended to whatever is pending).
    public func restore(_ items: [Item]) {
        queue.append(contentsOf: items)
    }

    /// Delivers the queue in order. Returns when every item is deposited or
    /// dropped; a retryable failure sleeps its backoff and continues.
    public func flush() async {
        guard !flushing else { return }
        flushing = true
        defer { flushing = false }
        while var item = queue.first {
            if Task.isCancelled { return }
            do {
                let result = try await client.deposit(room: item.room, item.payload)
                queue.removeFirst()
                onOutcome?(.deposited(result))
            } catch let error as P2PError {
                switch error {
                case .http(let status, _, _) where status == 413:
                    queue.removeFirst()
                    onOutcome?(.dropped(item, reason: "message_too_large"))
                case .http(let status, _, _) where status == 401 || status == 403:
                    queue.removeFirst()
                    onOutcome?(.dropped(item, reason: "unauthorized"))
                case .config(let why):
                    queue.removeFirst()
                    onOutcome?(.dropped(item, reason: why))
                default:
                    // 429, 507, 5xx, transport: back off and retry the same item
                    let delay = Backoff.mailbox(attempt: item.attempts)
                    item.attempts += 1
                    queue[0] = item
                    onOutcome?(.deferred(item, retryAfter: delay))
                    guard (try? await clock.sleep(for: delay)) != nil else { return }
                }
            } catch {
                let delay = Backoff.mailbox(attempt: item.attempts)
                item.attempts += 1
                queue[0] = item
                onOutcome?(.deferred(item, retryAfter: delay))
                guard (try? await clock.sleep(for: delay)) != nil else { return }
            }
        }
    }
}

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/p2p-swift.git