Reputation: 1654
I'm working on caching for my network module. My module will return an AnyCancallable
back to the caller for each request. If cached data is not available, I use URLSession.dataTaskPublisher
, it works fine with 2 events: data received and completion. If cached data is available, I will use a CurrentValueSubject
to create the AnyCancallable to return. I send both of the 2 events on this subject, but on the caller side, it only gets notified on the completion, no data.
cacheSubject.send(cachedData.data)
cacheSubject.send(completion: Subscribers.Completion<Error>.finished)
Removing the completion send and now it can receive data, but no completion notification.
Could someone please let me know what I'm doing wrong here?
Here is the full file in case you guys need it:
public class SSNetworkManager {
public static let shared = SSNetworkManager()
private var cache: [String: CachedData] = [:]
private let cacheSubject = CurrentValueSubject<Data, Error>(Data())
@discardableResult public func makeServiceCall<D: Decodable>(forRequest request: SSNetworkRequest<D>, onMainThread: Bool = true) -> AnyPublisher<D, Error>? {
guard let urlRequest = request.urlRequest else {
return nil
}
var cancelable: AnyPublisher<Data, Error>
if let url = urlRequest.url?.absoluteString,
let cachedData = cache[url],
cachedData.isValid {
cancelable = cacheSubject.eraseToAnyPublisher()
cacheSubject.send(cachedData.data)
cacheSubject.send(completion: Subscribers.Completion<Error>.finished)
} else {
cancelable = URLSession.shared.dataTaskPublisher(for: urlRequest).tryMap {[weak self] (data, response) -> Data in
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw SSNetworkError(httpCode: (response as? HTTPURLResponse)?.statusCode ?? 0, data: data)
}
if request.shouldCacheNow,
let url = urlRequest.url?.absoluteString {
self?.cache[url] = CachedData(data: data, expirationTime: request.cacheExpirationTime)
}
return data
}.eraseToAnyPublisher()
}
if onMainThread {
return cancelable
.receive(on: RunLoop.main)
.decode(type: D.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
} else {
return cancelable
.decode(type: D.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
}
fileprivate struct CachedData {
let data: Data
let expirationTime: Date
var isValid: Bool {
return Date().compare(expirationTime) != .orderedDescending
}
}
Upvotes: 2
Views: 2910
Reputation: 49620
This isn't the right case to use a subject. Instead, return the publisher, relevant to each case:
public class SSNetworkManager {
// ...
public func makeServiceCall<D: Decodable>(
forRequest request: SSNetworkRequest<D>,
onMainThread: Bool = true
) -> AnyPublisher<D, Error>? {
// consider just returning Empty().eraseToAnyPublisher() instead of nil
guard let urlRequest = request.urlRequest else {
return nil
}
var resultPublisher: AnyPublisher<D: Error>
if let url = urlRequest.url?.absoluteString,
let cachedData = cache[url],
cachedData.isValid {
resultPublisher = Just(cachedData.data)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
} else {
resultPublisher = URLSession.shared
.dataTaskPublisher(for: urlRequest)
.tryMap { ... }
.decode(type: D.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
return onMainThread
? resultPublisher
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
: resultPublisher
.eraseToAnyPublisher()
}
}
Upvotes: 1