How to properly pull from cache before remote using swift combine

Viewed 643

I perform many repeated requests in order to populate a field. I would like to cache the result and use the cached value the next time around.

public func getItem(_ id: String) -> AnyPublisher<Item?, Never> {
    if let item = itemCache[id] {
        return Just(item).eraseToAnyPublisher()
    }

    return downloadItem(id: id)
        .map { item in
            if let item = item {
                itemCache[id] = item
            }
            return item
        }
        .eraseToAnyPublisher()
    }
}

func downloadItem(_ id: String) -> AnyPublisher<Item?, Never> { ... }

And this is called like this:

Just(["a", "a", "a"]).map(getItem)

However, all the requests are calling downloadItem. downloadItem does return on the main queue. I also tried wrapping the entire getItem function into Deferred but that had the same result.

1 Answers

First, the issue was that the function is being evaluated and only a publisher is returned. So the cache check is evaluated each time before the network publisher is ever subscribed to. Using Deferred is the proper fix for that. However, that still didn't solve the problem.

The solution was instead to first cache a shared publisher while the network request is pending so all requests during the network call will use the same publisher, then when it's complete to cache a Just publisher for the all future calls:

public func getItem(_ id: String) -> AnyPublisher<Item?, Never> {
    if let publisher = self.publisherCache[id] {
        return publisher
    }

    let publisher = downloadItem(id)
        .handleEvents(receiveOutput: {
            // Re-cache a Just publisher once the network request finishes
            self.publisherCache[id] = Just($0).eraseToAnyPublisher()
        })
        .share() // Ensure the same publisher is returned from the cache
        .eraseToAnyPublisher()

    // Cache the publisher to be used while downloading is in progress
    self.publisherCache[id] = publisher

    return publisher
}

One note, is that downloadItem(id) is async and being recieved on the main loop. When I replaced downloadItem(id) with Just(Item()) for testing, this didn't work beause the entire publisher chain was evaluated on creation. Use Just(Item()).recieve(on: Runloop.main) to fix that while testing.

Related