rastrillo / native Public

Clone
git clone https://amadan.net/rastrillo/native

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import RastrilloNative
3
4/// The pause between attempts of a long-lived stream, so every app gets the
5/// same rules:
6/// - a server's Retry-After is a deadline that nothing crosses, including a
7/// connectivity change or a return to the foreground;
8/// - otherwise it waits out `Backoff`, cut short (and reset) by a
9/// connectivity change, with no timer at all while the path is down;
10/// - the backoff resets after a stream that stayed open longer than 5 s,
11/// timed from `opened()`, never from the start of the attempt.
12///
13/// Call `opened()` from the stream's `onOpen`, `failed(_:)` with the error
14/// that ended an attempt, and `pause()` before the next attempt.
15@MainActor public final class NativeReconnect {
16 private let connectivity: NativeConnectivity
17 private var backoff: Backoff
18 private let now: @MainActor () -> TimeInterval
19 private var notBefore: TimeInterval?
20
21 public init(connectivity: NativeConnectivity, backoff: Backoff = Backoff(),
22 now: @escaping @MainActor () -> TimeInterval = { ProcessInfo.processInfo.systemUptime }) {
23 self.connectivity = connectivity
24 self.backoff = backoff
25 self.now = now
26 }
27
28 /// The stream was accepted.
29 public func opened() { backoff.opened(at: now()) }
30
31 /// Records a server's Retry-After as a deadline, capped at 5 minutes.
32 public func failed(_ error: Error) {
33 guard case NativeClientError.rateLimited(let after?) = error, after > 0 else { return }
34 let deadline = now() + min(after, Backoff.retryAfterCap)
35 notBefore = max(notBefore ?? 0, deadline)
36 }
37
38 /// Waits before the next attempt. Returns the backoff delay chosen, for tests.
39 @discardableResult
40 public func pause() async -> TimeInterval {
41 let start = now()
42 backoff.ended(at: start)
43 let delay = backoff.next()
44 var remaining = delay
45 if let deadline = notBefore, deadline > start {
46 // Only the server's deadline is uninterruptible; cancellation still ends it.
47 try? await Task.sleep(nanoseconds: UInt64((deadline - start) * 1_000_000_000))
48 remaining -= now() - start
49 }
50 notBefore = nil
51 if Task.isCancelled { return delay }
52 if remaining > 0 || !connectivity.isPathSatisfied {
53 if await connectivity.wait(upTo: max(0, remaining)) { backoff.reset() }
54 }
55 return delay
56 }
57}
58