Skip to content

Instantly share code, notes, and snippets.

@herzi
Created September 27, 2020 17:29
Show Gist options
  • Save herzi/a6b4265a08bf14600da31734073055ae to your computer and use it in GitHub Desktop.
Save herzi/a6b4265a08bf14600da31734073055ae to your computer and use it in GitHub Desktop.
Combine vs. Monotonic Headers

Combine vs. Monotonic Properties

TLDR: How to serialize independent, heterogeneous HTTP requests using Combine and Foundation.

I have a view model that displays to pieces of information from an API, let's assume it's a user's bio and the latest posts from the user.

class ViewModel: ObservableObject {
    @Published var bio: String?
    @Published var posts: [Post]?
    private var subscriptions = [AnyCancellable]()
    let username: String
    
    init (username: String, api: API) {
        self.username = username
        
        api.fetchProfile(username: username)
            .assign(to: \.profile, on: self)
            .store(in: &subscriptions)
        api.fetchLatestPosts(username: username)
            .assign(to: \.latestPosts, on: self)
    }
}

As the requests don't use information from each other, a simple flatMap() would not be what I would expect to use here.

I figured the best way to solve this is to move this issue onto the level of the API client, so it can consider these requirements for any request on any view:

class API {
    // …
    
    func publisher<T: Decodable>(for type: T.Type,
                                 from url: URL)
        -> AnyPublisher<T?, Never>
    {
        let nonce = UInt64(Date.timeIntervalSinceReferenceDate * 1_000)
        var request = URLRequest(url: url)
        request.setValue(nonce.description,
                         forHTTPHeaderField: "Nonce")
        // Basically, I'm looking for a way to return a
        // publisher here that satisfies to the outlined
        // requirements.
        return URLSession.shared
            .dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: type, decoder: JSONDecoder())
            .map { $0 as Optional }  // Let's not make this too
            .replaceError(with: nil) // complicated with error handling.
            .eraseToAnyPublisher()
    }
    
    // …
}

I was considering this:

  • create a publisher for the nonce,
  • zip the nonce to the URL
  • map nonce+URL to URLRequest
  • flatMap using URLSession.
  • at some point in the chain, publish a new nonce for the next request.

But then I still wonder: How to make sure each request gets a different nonce, because it would be easy to subscribe two requests to the nonce generator and let them have the same nonce (which would not work).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment