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
import Foundation
@Observable
@MainActor
final class StoryStatusCache {
private struct CachedEntry {
let author: GrainStoryAuthor
let expiresAt: Date
}
private static let storyLifetime: TimeInterval = 86400 // 24 hours
private static let dateFormatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}()
private static let dateFormatterNoFrac: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
return formatter
}()
private static func parseDate(_ string: String) -> Date? {
dateFormatter.date(from: string) ?? dateFormatterNoFrac.date(from: string)
}
private var entries: [String: CachedEntry] = [:]
/// Live authors whose stories have not yet expired.
var authorsByDid: [String: GrainStoryAuthor] {
let now = Date()
return entries.compactMapValues { $0.expiresAt > now ? $0.author : nil }
}
var didsWithStories: Set<String> {
Set(authorsByDid.keys)
}
func hasStory(for did: String) -> Bool {
guard let entry = entries[did] else { return false }
return entry.expiresAt > Date()
}
func author(for did: String) -> GrainStoryAuthor? {
guard let entry = entries[did], entry.expiresAt > Date() else { return nil }
return entry.author
}
func update(from authors: [GrainStoryAuthor]) {
entries = Dictionary(uniqueKeysWithValues: authors.filter { $0.storyCount > 0 }.map { author in
let expiresAt: Date = if let latestAt = Self.parseDate(author.latestAt) {
latestAt.addingTimeInterval(Self.storyLifetime)
} else {
.distantPast
}
return (author.profile.did, CachedEntry(author: author, expiresAt: expiresAt))
})
}
/// Remove a specific author from the cache (e.g. after deleting their last story).
func remove(did: String) {
entries.removeValue(forKey: did)
}
/// Remove entries whose stories have expired. Call on app foreground and background.
func purgeExpired() {
let now = Date()
entries = entries.filter { $0.value.expiresAt > now }
}
/// Drop everything. The set of authors with live stories depends on who you
/// follow, so it doesn't carry across an account switch.
func clear() {
entries = [:]
}
}