summaryrefslogtreecommitdiff
path: root/Hutch/Networking
diff options
context:
space:
mode:
Diffstat (limited to 'Hutch/Networking')
-rw-r--r--Hutch/Networking/APICache.swift30
-rw-r--r--Hutch/Networking/APICacheKeys.swift43
-rw-r--r--Hutch/Networking/PasteService.swift19
-rw-r--r--Hutch/Networking/ProjectService.swift23
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(