Files
andros 15eb8acbd0 Network performance: ETag caching, partial Range fetches, progressive timeline streaming
- FeedCache actor: conditional GET with ETag/Last-Modified, returns 304 hits from cache
- PartialFeedFetcher: Range requests for header-only Discover fetches and date-filtered timeline fetches (2 requests instead of 3 per feed)
- TimelineFetcher: AsyncStream-based streaming emits posts as each feed arrives instead of waiting for all feeds
- TimelineViewModel: consumes stream progressively so posts appear immediately on first feed
- Discover uses header-only fetch (16 KB vs full file)
- maxConcurrentDownloads raised from 10 to 20
2026-05-16 10:12:41 +02:00

58 lines
2.1 KiB
Swift

import Foundation
/// In-memory store for HTTP conditional GET headers (ETag / Last-Modified).
///
/// When `FeedFetcher` receives a successful response it stores the server's
/// caching headers here. On the next request for the same URL it sends
/// `If-None-Match` / `If-Modified-Since`; a 304 response lets it return
/// the cached body without re-downloading or re-parsing.
///
/// `FeedCache.shared` is the default singleton. Tests can create isolated
/// instances (`FeedCache()`) to avoid cross-test pollution.
public actor FeedCache: Sendable {
public static let shared = FeedCache()
private struct Entry {
var etag: String?
var lastModified: String?
var content: String
}
private var store: [String: Entry] = [:]
// MARK: - Write
/// Stores caching headers and body for `url`.
/// Does nothing when neither `etag` nor `lastModified` is present
/// (no usable validator means a conditional GET would always get 200).
public func record(url: URL, etag: String?, lastModified: String?, content: String) {
guard etag != nil || lastModified != nil else { return }
store[url.absoluteString] = Entry(etag: etag, lastModified: lastModified, content: content)
}
/// Removes the cache entry for `url`. Call before a forced refresh.
public func invalidate(_ url: URL) {
store.removeValue(forKey: url.absoluteString)
}
// MARK: - Read
/// Returns the conditional request headers to include for `url`.
/// Both values are `nil` when no entry exists.
public func conditionalHeaders(for url: URL) -> (ifNoneMatch: String?, ifModifiedSince: String?) {
guard let entry = store[url.absoluteString] else { return (nil, nil) }
return (entry.etag, entry.lastModified)
}
/// Returns the previously cached body for `url`, or `nil` when not cached.
public func cachedContent(for url: URL) -> String? {
store[url.absoluteString]?.content
}
/// Returns `true` when a cache entry exists for `url`.
public func has(_ url: URL) -> Bool {
store[url.absoluteString] != nil
}
}