Home > Enterprise >  Combine: how to chain and then recombine one-to-many network queries
Combine: how to chain and then recombine one-to-many network queries

Time:06-16

I'm trying to understand how to chain and then recombine one-to-many network queries using Combine.

I have an initial request that retrieves some JSON data, decodes it and maps that to a list of IDs:

let _ = URLSession.shared
    .dataTaskPublisher(for: url)
    .receive(on: apiQueue)
    .map(\.data)
    .decode(type: MyMainResultType.self, decoder: JSONDecoder())
    .map { $0.results.map { $0.id } } // 'results' is a struct containing 'id', among others
    // .sink() and .store() omitted

This gives me the expected array of ints: [123, 456, ...]

For each of the ints I'd like to start another request that queries another endpoint using that ID as a parameter, retrieves some JSON, extracts an appropriate piece of data and then recombines that with the ID to give me a final list of [(id, otherData), ...].

The second request is working as a function in isolation, with its own sink() and store(), and also as an AnyPublisher<>.

I've tried any number of map { Publishers.Sequence ...}, .flatMap(), .combine() etc. but think that maybe my mental model of what's happening is incorrect.

What I think I should be doing is map() each int to the secondary details request publisher, then doing a flatMap() to get back a single publisher, and collect()ing all the results, possibly with another map to bring in the ID, but nothing seems to give me a simple list, as described above, at the end.

How can I take my list of ints and spawn a number of further requests, waiting until all of them have completed, and then reassemble the id and the additional info into a single Combine chain?

TIA!

CodePudding user response:

After the existing pipeline, you should first flatMap to Publishers.Sequence:

.flatMap(\.publisher)

This changes turns your publisher from one that publishes arrays of things, into one that publishes those array elements.

Then do another flat map to the URL session data task publisher, with all the steps to extract otherData attached. Note that at the very end is where we associate id to otherData:

.flatMap { id in
    URLSession.shared.dataTaskPublisher(
        // as an example
        for: URL(string: "https://example.com/?id=\(id)")!
    ).receive(on: apiQueue)
    .map(\.data)
    .decode(type: Foo.self, decoder: JSONDecoder())
    .map { (id, $0.otherData) } // associate id with otherData
}

Then you can collect() to turn it into a publisher that publishes an array only.

Full version:

// this is of type AnyPublisher<[(Int, Int)], Error>
let _ = URLSession.shared
    .dataTaskPublisher(for: url)
    .receive(on: apiQueue)
    .map(\.data)
    .decode(type: MyMainResultType.self, decoder: JSONDecoder())
    .map { $0.results.map { $0.id } }
    .flatMap(\.publisher)
    .flatMap { id in
        URLSession.shared.dataTaskPublisher(
            // as an example
            for: URL(string: "https://example.com/?id=\(id)")!
        ).receive(on: apiQueue)
        .map(\.data)
        .decode(type: Foo.self, decoder: JSONDecoder())
        .map { (id, $0.otherData) } // associate id with otherData
    }
    .collect()
    .eraseToAnyPublisher()
  • Related