Hello all.
I'm trying to implement a sort of cache-aside pattern for working with a slow web service and a local database cache.
I thought that this sounded like a sensible use for Combine Publisher
s since different values could be produced over a user-noticable period of time.
Would anyone be able to comment on whether the below looks like an acceptable way of implementing this system?
The only downside I can see immediately is that you can't cancel the created Task
, though I'm not sure that it is actually a problem.
Thank you.
struct WebReport {...}
struct Report {...}
class WebService {
func reports() async throws -> [WebReport] {...}
}
class Database {
func reports() async throws -> [Report] {...}
func updateWithWebReports(_ reports: [WebReport]) async throws {...}
}
struct RepoError: Error {}
struct Repo {
private let web: WebService = WebService()
private let db: Database = Database()
func reports() -> AnyPublisher<[Report], RepoError> {
let subject = CurrentValueSubject<[Report], Error>([])
refreshingTask(subject: subject,
loadAction: { try await db.reports() },
refreshAction: refreshReports)
return subject.eraseToAnyPublisher()
}
private func refreshingTask<T>(
subject: CurrentValueSubject<T, RepoError>,
loadAction: @escaping () async throws -> T,
refreshAction: @escaping () async throws -> Void
) {
Task {
do {
// 1. Start loading from both database and web service
async let cacheResult = loadAction()
async let refreshResult: Void = refreshAction()
// 2. Send database value
subject.send(try await cacheResult)
// 3. Wait for web service update
try await refreshResult
// 4. Re-run database query
subject.send(try await loadAction())
subject.send(completion: .finished)
}
catch {
subject.send(completion: .failure(RepoError()))
}
}
}
private func refreshReports() async throws {
let webReports = try await web.reports()
try await db.updateWithWebReports(webReports)
}
}