Reputation: 635
I have started using Combine quite recently and I am trying to create a repository that returns the combination of several data sources. To do that, each data source loads its own data in a quite simple load method:
func loadData() -> AnyPublisher<DataObject, Error>
To combine the data sources I thought of using combineLatest
, since it'll wait for the data sources to finish loading and then it'll publish either a combined set with the data or an error indicating that it failed:
func loadData() -> AnyPublisher<[DataObject], Error> {
return dataSource1.loadData()
.combineLatest(dataSource2.loadData())
.map { $0.0 + $0.1 }
.eraseToAnyPublisher()
}
Overall the behavior of it seems alright, I can call repository.loadData()
and I'll get an array that includes the data for both items. However, that's not the case if any of the data sources fail. In that case, the load method will return an error regardless of whether the other data source succeeded.
Is there a standard or recommended way to collect all the errors when combining publishers? In my use context I'd like to be able to discard the error only if both publishers failed, but go through and succeed if only one of them does.
Upvotes: 1
Views: 2086
Reputation: 385500
I think you're saying you want this:
If dataSource1
fails and dataSource2
produces output, discard the failure from dataSource1
and pass along just the output from dataSource2
.
If dataSource1
produces output and dataSource2
fails, pass along just the output from dataSource1
and discard the failure from dataSource2
.
If both dataSource1
and dataSource2
produce output, pass along the combined outputs.
If both dataSource1
and dataSource2
fail, pass along one of the errors.
I assume each data source produces at most one output. Here's a test setup:
typealias DataObject = String
struct DataSource {
var result: Result<DataObject, Error>
func loadData() -> AnyPublisher<DataObject, Error> {
return result.publisher.eraseToAnyPublisher()
}
}
We do want to use combineLatest
, but we can't let either input to combineLatest
fail, because that will make combineLatest
fail. We only want combineLatest
to fail if both data sources fail. So we need a way to pass a error into combineLatest
as an output of one of its input publishers, instead of as a failure of one of its input publishers.
We do that by transforming each input publisher to have an Output
of Result<DataObject, Error>
and a Failure
of Never
.
func combine(
_ source1: DataSource,
_ source2: DataSource
) -> AnyPublisher<[DataObject], Error> {
let ds1 = source1.loadData()
.map { Result.success($0) }
.catch { Just(Result.failure($0)) }
let ds2 = source2.loadData()
.map { Result.success($0) }
.catch { Just(Result.failure($0)) }
let combo = ds1.combineLatest(ds2)
.tryMap { r1, r2 -> [DataObject] in
switch (r1, r2) {
case (.success(let s1), .success(let s2)): return [s1, s2]
case (.success(let s1), .failure(_)): return [s1]
case (.failure(_), .success(let s2)): return [s2]
case (.failure(let f1), .failure(_)): throw f1
}
}
return combo.eraseToAnyPublisher()
}
Let's test it:
struct MockError: Error { }
combine(.init(result: .success("hello")), .init(result: .success("world")))
.sink(
receiveCompletion: { print($0) },
receiveValue: { print($0) })
// Output:
// ["hello", "world"]
// finished
combine(.init(result: .success("hello")), .init(result: .failure(MockError())))
.sink(
receiveCompletion: { print($0) },
receiveValue: { print($0) })
// Output:
// ["hello"]
// finished
combine(.init(result: .failure(MockError())), .init(result: .success("world")))
.sink(
receiveCompletion: { print($0) },
receiveValue: { print($0) })
// Output:
// ["world"]
// finished
combine(.init(result: .failure(MockError())), .init(result: .failure(MockError())))
.sink(
receiveCompletion: { print($0) },
receiveValue: { print($0) })
// Output:
// failure(__lldb_expr_28.MockError())
Upvotes: 6