1

I am currently using combine and URLSession.shared.dataTaskPublisher to do a request dependency. The first call gets an id either from cache or from a database which is then used in the second call. The application I am making can call the first function to get the id required at any time and multiple times at the same time. What I would like to do is when the First call goes out to the database to get the id for all other calls to wait before calling on this function. Is it ideal to have lock before calling on the function or if there is a better way that I am not thinking about? Thank you for your help!

Edit: I understand Combine uses a locking mechanism going down the pipeline, however, would it follow the same rule if multiple functions are calling the getId function in terms of waiting for a response? I am thinking of the approach of if I am the second or third function calling getId and the function is already working to get the id, I want to wait until I can just pull it from the cache.

Code:

override func viewDidLoad() {
    super.viewDidLoad()

    if let url = URL(string: "https://some url") {

    self.cancellable = self.getId()
    .flatMap { id in
        return self.testDetails(for: id)
    }
    .sink(receiveCompletion: { completion in

    }) { testObject in
        print(testObject.name)
    }

    }
}

func getId() -> AnyPublisher<String, ClientError> {
//lock here? so other calls can wait to check if the id is in the cache once unlocked?
//do some stuff to get from cache
if cachedId != nil {
   return Just.init(cachedId)
            .mapError({ never -> ClientError in
                ClientError.init("Some error has occured")
            })
            .eraseToAnyPublisher() 
 }

return URLSession.shared.dataTaskPublisher(for: url)
    .map { $0.data }
    .decode(type: [TestObject].self, decoder: JSONDecoder())
    .tryMap { testObjects in
        guard let id = testObjects.first?.id else {
            throw ClientError.init("Some error has occured")
        }
        return id
    }
    .eraseToAnyPublisher()
}

func testDetails(for id: Int) -> AnyPublisher<Post, Error> {
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!
    return URLSession.shared.dataTaskPublisher(for: url)
        .mapError { $0 as Error }
        .map { $0.data }
        .decode(type: Post.self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}
paul590
  • 1,385
  • 1
  • 22
  • 43
  • 1
    You don't use / need a lock. This is exactly what Combine is _for_. You just serialize the fetches. If there are just two of them, make the second one a `.flatMap` on the first one. – matt Apr 29 '21 at 22:14
  • @matt thank you for your response. What would happen if I have three functions that call to getId? If getId is fetching the id, what can I do to not have getId go out to get another id and instead wait for the response of that one since it will get cached? – paul590 Apr 30 '21 at 13:04
  • 1
    Combine is a dataflow model. We do not proceed down the pipeline the next step until this step produces data. There is no cache. If something needs to flow down to later steps, your job is to flow it. — See my https://stackoverflow.com/questions/61841254/combine-framework-how-to-process-each-element-of-array-asynchronously-before-pr to see me in the process of understanding this myself. :) – matt Apr 30 '21 at 19:27
  • 1
    I have written an extensive introduction to combine that puts your head in the right place (I hope): https://www.apeth.com/UnderstandingCombine/ Please read it before proceeding, I really think it will help. – matt Apr 30 '21 at 19:29
  • @matt thank you for your help! in any case, it sounds to me that if I have multiple functions calling this same function, I will need to add a lock manually before they access the dataflow, that way I can ensure we have retrieved a token and store it in cache since the dataflow will check if there is anything in the cache before going out to get one from the serve – paul590 Apr 30 '21 at 19:30
  • @matt awesome thank you for your help! I will read your documentation! – paul590 Apr 30 '21 at 19:31
  • 1
    I would not add any locks! The whole way you are phrasing this still seems to suggest you're not adapting your overall architecture to the way combine works. – matt Apr 30 '21 at 19:32
  • @matt thank you! I believe I am stuck on the mentality as I had before using Combine. I will need to think of a better way to approach this. Since my confusion comes from how can I get this id at any moment and in different parts of the application – paul590 Apr 30 '21 at 19:40
  • 1
    In my opinion the best way is to keep using combine and keep the data flowing. You do not "get" the id and then "use" it. You _flow_ the data, each time you get it, to all the places in your app that need it. Make the _whole_ app "reactive" in this way. Think in terms of "source of truth" that publishes and clients who subscribe to it as needed. – matt Apr 30 '21 at 20:22
  • @matt I see what you mean thank you very much this helps me a lot! – paul590 Apr 30 '21 at 20:42
  • @matt thank you again for your help! While the thought process you gave me helps me a ton, my next question would be how can I have a 'source of truth' idea if the id expires every 2 hours for example? similar to a header token? then in that scenario, we would need to approach it differently since I can have a call that requires this id, but if it's expired we need to go and get a new one. This is what I am struggling with. using combine with this hurdle – paul590 Apr 30 '21 at 22:26
  • 1
    I've never faced this exact issue, but you have a _pipeline_ that you can create-and-fire at will. I presume this is a one-shot pipeline because once you've fired a data task publisher once, it's complete. But you could just make a cron job (aka a Timer) that comes along every hour and just creates-and-runs the pipeline again, thus updating the id and (as we said) percolating that out to everyone who needs it. – matt Apr 30 '21 at 22:36
  • @matt thank you for your help and brainstorming ideas with me I appreciate it! And also for pointing me to use Combine so far I am enjoying it! – paul590 May 01 '21 at 03:14
  • 1
    Just noting the documentation from flatMap here: >> Transforms all elements from an upstream publisher into a new publisher up to a maximum number of publishers you specify. << I did not know flatMap can do this, I only knew its behavior in relation to arrays.. searched so long for this! Returning new publishers is what I missed. – Fabian May 02 '21 at 23:12
  • @Fabian thank you for this information this helps me clear flatMap a lot more! – paul590 May 03 '21 at 19:41

0 Answers0