~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1
main
78 lines · 2.5 KB Raw History
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 = [:]
    }
}