Repository navigation
Expand file tree
/
Copy pathTokenState.swift
More file actions
51 lines (44 loc) · 1.78 KB
/
Copy pathTokenState.swift
File metadata and controls
51 lines (44 loc) · 1.78 KB
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
//
// TokenState.swift
// Realtime
//
// Created by Guilherme Souza on 06/10/26.
//
import Foundation
import Helpers
/// The access token channels join with, and the bookkeeping that keeps refreshes in order.
struct TokenState: Sendable {
private(set) var token: String?
/// The `exp` claim of ``token`` when it is a JWT.
private(set) var expiresAt: Date?
private(set) var generation = 0
init() {}
/// Marks the start of a refresh. Only a result applied with this generation counts.
mutating func beginRefresh() -> Int {
generation += 1
return generation
}
/// Stores `token` and returns whether it changed. A `nil`, an unchanged token, or a result
/// from an older refresh leaves the state as it was.
mutating func apply(_ token: String?, generation: Int) -> Bool {
guard generation == self.generation, let token, token != self.token else { return false }
self.token = token
expiresAt = (JWT.decodePayload(token)?["exp"] as? TimeInterval).map(
Date.init(timeIntervalSince1970:))
return true
}
/// How long until the token should be refreshed: `leeway` before `exp`, never negative and at
/// most ``maximumRefreshDelay``. `nil` when the token has no expiry to plan around.
func refreshDelay(now: Date, leeway: Duration) -> Duration? {
guard let expiresAt else { return nil }
// Clamped before the conversion: `Int(_:)` traps on a value out of range, and `exp` is
// whatever number the token carries.
let remaining = min(
max(expiresAt.timeIntervalSince(now).rounded(), 0),
Self.maximumRefreshDelay.timeInterval
)
return max(.seconds(Int(remaining)) - leeway, .zero)
}
/// The longest wait the refresh timer is set for: one year.
static let maximumRefreshDelay: Duration = .seconds(365 * 24 * 60 * 60)
}