diff options
Diffstat (limited to 'Hutch/Networking')
| -rw-r--r-- | Hutch/Networking/APICache.swift | 30 | ||||
| -rw-r--r-- | Hutch/Networking/APICacheKeys.swift | 43 | ||||
| -rw-r--r-- | Hutch/Networking/PasteService.swift | 19 | ||||
| -rw-r--r-- | Hutch/Networking/ProjectService.swift | 23 |
4 files changed, 91 insertions, 24 deletions
diff --git a/Hutch/Networking/APICache.swift b/Hutch/Networking/APICache.swift index 9c1a262..05cf80e 100644 --- a/Hutch/Networking/APICache.swift +++ b/Hutch/Networking/APICache.swift @@ -35,7 +35,7 @@ struct CacheEntryMetadata: Codable, Sendable, Equatable { let schemaVersion: Int let payloadSize: Int - func isExpired(now: Date = Date()) -> Bool { + nonisolated func isExpired(now: Date = Date()) -> Bool { expiresAt <= now } } @@ -83,7 +83,7 @@ struct APICacheConfiguration: Sendable { var memoryEntryLimit: Int var schemaVersion: Int - static func accountScoped(accountID: String) -> APICacheConfiguration { + nonisolated static func accountScoped(accountID: String) -> APICacheConfiguration { let base = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first ?? URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) return APICacheConfiguration( @@ -98,7 +98,7 @@ struct APICacheConfiguration: Sendable { ) } - static func temporary(directory: URL) -> APICacheConfiguration { + nonisolated static func temporary(directory: URL) -> APICacheConfiguration { APICacheConfiguration( directory: directory, maxCacheSizeBytes: 4 * 1024 * 1024, @@ -126,15 +126,13 @@ actor PersistentAPICache: APICache { } private let configuration: APICacheConfiguration - private let fileManager: FileManager private var memoryEntries: [String: APICacheEntry] = [:] private var memoryOrder: [String] = [] private var knownMetadata: [String: CacheEntryMetadata] = [:] private var writeCountSincePrune = 0 - init(configuration: APICacheConfiguration, fileManager: FileManager = .default) { + init(configuration: APICacheConfiguration) { self.configuration = configuration - self.fileManager = fileManager } func read(cacheKey: String) async throws -> APICacheEntry { @@ -147,13 +145,13 @@ actor PersistentAPICache: APICache { } let url = fileURL(for: cacheKey) - guard fileManager.fileExists(atPath: url.path) else { + guard FileManager.default.fileExists(atPath: url.path) else { throw APICacheError.miss } var stored = try decodeEntry(from: url) guard stored.metadata.schemaVersion == configuration.schemaVersion else { - try? fileManager.removeItem(at: url) + try? FileManager.default.removeItem(at: url) throw APICacheError.miss } @@ -223,7 +221,7 @@ actor PersistentAPICache: APICache { memoryEntries.removeValue(forKey: cacheKey) memoryOrder.removeAll { $0 == cacheKey } knownMetadata.removeValue(forKey: cacheKey) - try? fileManager.removeItem(at: fileURL(for: cacheKey)) + try? FileManager.default.removeItem(at: fileURL(for: cacheKey)) } func removeByPrefix(_ prefix: String) async { @@ -237,7 +235,7 @@ actor PersistentAPICache: APICache { memoryEntries.removeAll() memoryOrder.removeAll() knownMetadata.removeAll() - try? fileManager.removeItem(at: configuration.directory) + try? FileManager.default.removeItem(at: configuration.directory) } func pruneExpired(now: Date = Date()) async { @@ -289,7 +287,7 @@ actor PersistentAPICache: APICache { private func loadKnownMetadataIfNeeded() async { guard knownMetadata.isEmpty else { return } - guard let urls = try? fileManager.contentsOfDirectory( + guard let urls = try? FileManager.default.contentsOfDirectory( at: configuration.directory, includingPropertiesForKeys: nil ) else { return } @@ -301,8 +299,8 @@ actor PersistentAPICache: APICache { } private func ensureDirectoryExists() throws { - if !fileManager.fileExists(atPath: configuration.directory.path) { - try fileManager.createDirectory( + if !FileManager.default.fileExists(atPath: configuration.directory.path) { + try FileManager.default.createDirectory( at: configuration.directory, withIntermediateDirectories: true ) @@ -315,13 +313,13 @@ actor PersistentAPICache: APICache { .appendingPathExtension("json") } - private static func payloadHash(_ data: Data) -> String { + nonisolated private static func payloadHash(_ data: Data) -> String { SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() } } extension JSONEncoder { - static var srhtCache: JSONEncoder { + nonisolated static var srhtCache: JSONEncoder { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 return encoder @@ -329,7 +327,7 @@ extension JSONEncoder { } extension JSONDecoder { - static var srhtCache: JSONDecoder { + nonisolated static var srhtCache: JSONDecoder { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return decoder diff --git a/Hutch/Networking/APICacheKeys.swift b/Hutch/Networking/APICacheKeys.swift index 5e57a51..d0cffa6 100644 --- a/Hutch/Networking/APICacheKeys.swift +++ b/Hutch/Networking/APICacheKeys.swift @@ -51,6 +51,14 @@ enum APICacheKeys { make([SRHTService.todo.rawValue, "tracker-labels", "tracker:\(trackerRid)"]) } + static func trackers(cursor: String? = nil) -> String { + make([SRHTService.todo.rawValue, "trackers", cursor.map { "cursor:\($0)" }]) + } + + static func tickets(trackerRid: String, cursor: String? = nil) -> String { + make([SRHTService.todo.rawValue, "tickets", "tracker:\(trackerRid)", cursor.map { "cursor:\($0)" }]) + } + static func builds(cursor: String? = nil, filter: String? = nil) -> String { make([SRHTService.builds.rawValue, "jobs", cursor.map { "cursor:\($0)" }, filter.map { "filter:\($0)" }]) } @@ -71,6 +79,37 @@ enum APICacheKeys { make([SRHTService.todo.rawValue, "user-trackers", normalize(owner), cursor.map { "cursor:\($0)" }]) } + static func projects(cursor: String? = nil) -> String { + make([SRHTService.hub.rawValue, "projects", cursor.map { "cursor:\($0)" }]) + } + + static func projectDetail(rid: String, mailingListsCursor: String? = nil, sourcesCursor: String? = nil, trackersCursor: String? = nil) -> String { + make([ + SRHTService.hub.rawValue, + "project", + "rid:\(rid)", + mailingListsCursor.map { "ml:\($0)" }, + sourcesCursor.map { "src:\($0)" }, + trackersCursor.map { "trk:\($0)" } + ]) + } + + static func homeJobs(actor: String) -> String { + make(["home", "jobs", normalize(actor)]) + } + + static func homeTrackerTickets(owner: String, tracker: String) -> String { + make(["home", "tickets", normalize(owner), normalize(tracker)]) + } + + static func inboxSubscriptions(cursor: String? = nil) -> String { + make([SRHTService.lists.rawValue, "subscriptions", cursor.map { "cursor:\($0)" }]) + } + + static func inboxThreads(listRid: String, cursor: String? = nil) -> String { + make([SRHTService.lists.rawValue, "threads", "list:\(listRid)", cursor.map { "cursor:\($0)" }]) + } + static func pasteList(cursor: String? = nil) -> String { make([SRHTService.paste.rawValue, "pastes", cursor.map { "cursor:\($0)" }]) } @@ -102,4 +141,8 @@ enum APICacheTTLs { static let movingRefFileContent: TimeInterval = 10 * 60 static let userProfile: TimeInterval = 30 * 60 static let status: TimeInterval = 5 * 60 + static let homeDashboard: TimeInterval = 2 * 60 + static let inboxSummary: TimeInterval = 2 * 60 + static let projectList: TimeInterval = 10 * 60 + static let projectDetail: TimeInterval = 10 * 60 } diff --git a/Hutch/Networking/PasteService.swift b/Hutch/Networking/PasteService.swift index b9aa530..92a618e 100644 --- a/Hutch/Networking/PasteService.swift +++ b/Hutch/Networking/PasteService.swift @@ -109,13 +109,17 @@ final class PasteService: Sendable { let variables = cursor.map { ["cursor": $0 as any Sendable] } let result: PasteListResponse if useCache, cursor == nil { - result = try await client.executeAndCache( + let cached = try await client.executeCached( service: .paste, query: Self.listQuery, variables: variables, responseType: PasteListResponse.self, - cacheKey: Self.cacheKey + cacheKey: APICacheKeys.pasteList(cursor: cursor), + resourceType: .pasteList, + ttl: APICacheTTLs.ticketList, + policy: .cacheFirstThenRefresh ) + result = cached.value } else { result = try await client.execute( service: .paste, @@ -127,8 +131,8 @@ final class PasteService: Sendable { return result.pastes ?? PasteListPage(results: [], cursor: nil) } - func loadCachedPastes() -> PasteListPage? { - guard let data = client.responseCache.get(forKey: Self.cacheKey) else { + func loadCachedPastes() async -> PasteListPage? { + guard let data = await client.cachedPayload(forKey: APICacheKeys.pasteList()) ?? client.responseCache.get(forKey: Self.cacheKey) else { return nil } @@ -171,6 +175,7 @@ final class PasteService: Sendable { }, responseType: CreatePasteResponse.self ) + await invalidatePasteCaches() return result.create } @@ -181,6 +186,7 @@ final class PasteService: Sendable { variables: ["id": id, "visibility": visibility.rawValue], responseType: UpdatePasteResponse.self ) + await invalidatePasteCaches() return result.update } @@ -191,6 +197,7 @@ final class PasteService: Sendable { variables: ["id": id], responseType: DeletePasteResponse.self ) + await invalidatePasteCaches() return result.delete } @@ -208,6 +215,10 @@ final class PasteService: Sendable { return (draft.filename, data) } } + + private func invalidatePasteCaches() async { + await client.invalidateCache(prefix: APICacheKeys.prefix(SRHTService.paste.rawValue, "pastes")) + } } private struct PasteListResponse: Decodable, Sendable { diff --git a/Hutch/Networking/ProjectService.swift b/Hutch/Networking/ProjectService.swift index 5e0e061..44320ca 100644 --- a/Hutch/Networking/ProjectService.swift +++ b/Hutch/Networking/ProjectService.swift @@ -287,12 +287,17 @@ struct ProjectService: Sendable { variables["cursor"] = cursor } - let response = try await client.execute( + let cached = try await client.executeCached( service: .hub, query: Self.projectsQuery, variables: variables.isEmpty ? nil : variables, - responseType: ProjectPageResponse.self + responseType: ProjectPageResponse.self, + cacheKey: APICacheKeys.projects(cursor: cursor), + resourceType: .userProfile, + ttl: APICacheTTLs.projectList, + policy: .cacheFirstThenRefresh ) + let response = cached.value results.append(contentsOf: response.me.projects.results) guard let nextCursor = response.me.projects.cursor else { @@ -324,12 +329,22 @@ struct ProjectService: Sendable { variables["trackersCursor"] = trackersCursor } - let response = try await client.execute( + let cached = try await client.executeCached( service: .hub, query: Self.projectDetailQuery, variables: variables, - responseType: ProjectDetailResponse.self + responseType: ProjectDetailResponse.self, + cacheKey: APICacheKeys.projectDetail( + rid: rid, + mailingListsCursor: mailingListsCursor, + sourcesCursor: sourcesCursor, + trackersCursor: trackersCursor + ), + resourceType: .userProfile, + ttl: APICacheTTLs.projectDetail, + policy: .cacheFirstThenRefresh ) + let response = cached.value guard let project = response.project else { throw SRHTError.decodingError( |
