diff options
| author | Christian Cleberg <[email protected]> | 2026-05-06 20:41:49 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-05-06 20:41:49 -0500 |
| commit | 205cf2687ce48f3d004792ff223422a86cc6bdbc (patch) | |
| tree | 7edb78cede354e79aa8abd764ed9c5047e0ee168 /Hutch/Views/Home | |
| parent | 57e4f34b4613c09beb0cb757ac2ba2b43cc04daf (diff) | |
| download | hutch-3.3.1.tar.gz hutch-3.3.1.tar.bz2 hutch-3.3.1.zip | |
feat(cache): persist read-only API responsesv3.3.1
Add a bounded stale-while-revalidate cache at the Sourcehut API boundary with
stable keys, centralized TTLs, request coalescing, payload hashing, and LRU disk
pruning.
Cache high-value read-only repo, build, ticket, project, profile, paste, and
Home/Work Queue data while keeping mutations network-only and invalidating
related prefixes after successful writes.
Add focused cache tests and implementation notes.
Diffstat (limited to 'Hutch/Views/Home')
| -rw-r--r-- | Hutch/Views/Home/HomeViewModel.swift | 63 |
1 files changed, 52 insertions, 11 deletions
diff --git a/Hutch/Views/Home/HomeViewModel.swift b/Hutch/Views/Home/HomeViewModel.swift index 142eee4..2cd4869 100644 --- a/Hutch/Views/Home/HomeViewModel.swift +++ b/Hutch/Views/Home/HomeViewModel.swift @@ -578,6 +578,7 @@ final class HomeViewModel { ], responseType: UnassignResponse.self ) + await invalidateHomeRelatedCaches() assignedTickets.removeAll { $0.id == ticket.id } persistNeedsAttentionSnapshot() } catch { @@ -595,6 +596,7 @@ final class HomeViewModel { variables: ["id": build.job.id], responseType: CancelBuildResponse.self ) + await invalidateHomeRelatedCaches() if let index = recentBuilds.firstIndex(where: { $0.id == build.id }) { let updatedJob = JobSummary( id: build.job.id, @@ -684,12 +686,16 @@ final class HomeViewModel { private func loadRecentJobs() async -> Result<[HomeJobPayload], Error> { do { - let response = try await client.execute( + let cached = try await client.executeCached( service: .builds, query: Self.jobsQuery, - responseType: HomeJobsResponse.self + responseType: HomeJobsResponse.self, + cacheKey: APICacheKeys.homeJobs(actor: currentUser.canonicalName), + resourceType: .buildList, + ttl: APICacheTTLs.homeDashboard, + policy: .cacheFirstThenRefresh ) - return .success(response.jobs.results) + return .success(cached.value.jobs.results) } catch { return .failure(error) } @@ -780,12 +786,17 @@ final class HomeViewModel { variables["cursor"] = cursor } - let response = try await client.execute( + let cached = try await client.executeCached( service: .lists, query: Self.inboxSubscriptionsQuery, variables: variables.isEmpty ? nil : variables, - responseType: HomeInboxSubscriptionsResponse.self + responseType: HomeInboxSubscriptionsResponse.self, + cacheKey: APICacheKeys.inboxSubscriptions(cursor: cursor), + resourceType: .ticketList, + ttl: APICacheTTLs.inboxSummary, + policy: .cacheFirstThenRefresh ) + let response = cached.value subscriptions.append(contentsOf: response.subscriptions.results) guard let nextCursor = response.subscriptions.cursor else { @@ -809,12 +820,17 @@ final class HomeViewModel { variables["cursor"] = cursor } - let response = try await client.execute( + let cached = try await client.executeCached( service: .lists, query: Self.inboxListThreadsQuery, variables: variables, - responseType: HomeInboxListThreadsResponse.self + responseType: HomeInboxListThreadsResponse.self, + cacheKey: APICacheKeys.inboxThreads(listRid: mailingList.rid, cursor: cursor), + resourceType: .ticketList, + ttl: APICacheTTLs.inboxSummary, + policy: .cacheFirstThenRefresh ) + let response = cached.value let unreadThreadSummaries = response.list.threads.results.compactMap { thread -> InboxThreadSummary? in let summary = InboxThreadSummary( @@ -876,12 +892,17 @@ final class HomeViewModel { variables["cursor"] = cursor } - let response = try await client.execute( + let cached = try await client.executeCached( service: .todo, query: Self.trackersQuery, variables: variables.isEmpty ? nil : variables, - responseType: HomeTrackersResponse.self + responseType: HomeTrackersResponse.self, + cacheKey: APICacheKeys.trackers(cursor: cursor), + resourceType: .ticketList, + ttl: APICacheTTLs.ticketList, + policy: .cacheFirstThenRefresh ) + let response = cached.value allTrackers.append(contentsOf: response.trackers.results) guard let nextCursor = response.trackers.cursor else { @@ -925,7 +946,7 @@ final class HomeViewModel { } private func fetchAssignedTickets(for tracker: TrackerSummary) async throws -> [HomeAssignedTicket] { - let response = try await client.execute( + let cached = try await client.executeCached( service: .todo, query: Self.trackerTicketsQuery, variables: [ @@ -934,8 +955,18 @@ final class HomeViewModel { : tracker.owner.canonicalName, "tracker": tracker.name ], - responseType: HomeTrackerTicketsResponse.self + responseType: HomeTrackerTicketsResponse.self, + cacheKey: APICacheKeys.homeTrackerTickets( + owner: tracker.owner.canonicalName.hasPrefix("~") + ? String(tracker.owner.canonicalName.dropFirst()) + : tracker.owner.canonicalName, + tracker: tracker.name + ), + resourceType: .ticketList, + ttl: APICacheTTLs.ticketList, + policy: .cacheFirstThenRefresh ) + let response = cached.value return response.user.tracker.tickets.results.compactMap { payload in guard payload.status.isOpen else { @@ -970,6 +1001,7 @@ final class HomeViewModel { ], responseType: StatusEventResponse.self ) + await invalidateHomeRelatedCaches() assignedTickets.removeAll { $0.id == ticket.id } persistNeedsAttentionSnapshot() } catch { @@ -977,6 +1009,15 @@ final class HomeViewModel { } } + private func invalidateHomeRelatedCaches() async { + await client.invalidateCache(prefix: APICacheKeys.prefix("home")) + await client.invalidateCache(prefix: APICacheKeys.prefix(SRHTService.todo.rawValue, "tickets")) + await client.invalidateCache(prefix: APICacheKeys.prefix(SRHTService.todo.rawValue, "ticket")) + await client.invalidateCache(prefix: APICacheKeys.prefix(SRHTService.todo.rawValue, "trackers")) + await client.invalidateCache(prefix: APICacheKeys.prefix(SRHTService.builds.rawValue, "jobs")) + await client.invalidateCache(prefix: APICacheKeys.prefix(SRHTService.builds.rawValue, "job")) + } + private func persistNeedsAttentionSnapshot() { let failedBuildCount = recentFailedBuilds().count NeedsAttentionSnapshotStore.save( |
