From 36807fbfb697ff760b746e06544d2fcf7f9f0326 Mon Sep 17 00:00:00 2001 From: samgilmore <30483214+samgilmore@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:08:23 -0400 Subject: [PATCH] Add basic caching --- Sources/SwiftNetKit/BaseRequest.swift | 5 ++- Sources/SwiftNetKit/CacheConfiguration.swift | 25 ++++++++++++ Sources/SwiftNetKit/NetworkService.swift | 35 ++++++++++++++--- .../Protocols/RequestProtocol.swift | 3 ++ .../NetworkServiceTests.swift | 39 +++++++++++++++++++ 5 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 Sources/SwiftNetKit/CacheConfiguration.swift diff --git a/Sources/SwiftNetKit/BaseRequest.swift b/Sources/SwiftNetKit/BaseRequest.swift index bfdaf46..7edaa53 100644 --- a/Sources/SwiftNetKit/BaseRequest.swift +++ b/Sources/SwiftNetKit/BaseRequest.swift @@ -15,18 +15,21 @@ public struct BaseRequest: RequestProtocol { let parameters: [String : Any]? let headers: [String : String]? let body: RequestBody? + let cacheConfiguration: CacheConfiguration? init( url: URL, method: MethodType, parameters: [String : Any]? = nil, headers: [String : String]? = nil, - body: RequestBody? = nil + body: RequestBody? = nil, + cacheConfiguration: CacheConfiguration? = nil ) { self.url = url self.method = method self.parameters = parameters self.headers = headers self.body = body + self.cacheConfiguration = cacheConfiguration } } diff --git a/Sources/SwiftNetKit/CacheConfiguration.swift b/Sources/SwiftNetKit/CacheConfiguration.swift new file mode 100644 index 0000000..fd9be55 --- /dev/null +++ b/Sources/SwiftNetKit/CacheConfiguration.swift @@ -0,0 +1,25 @@ +// +// CacheConfiguration.swift +// +// +// Created by Sam Gilmore on 7/19/24. +// + +import Foundation + +public struct CacheConfiguration { + var memoryCapacity: Int + var diskCapacity: Int + var diskPath: String? + var cachePolicy: URLRequest.CachePolicy + + public init(memoryCapacity: Int = 20 * 1024 * 1024, // 20 MB + diskCapacity: Int = 100 * 1024 * 1024, // 100 MB + diskPath: String? = nil, + cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy) { + self.memoryCapacity = memoryCapacity + self.diskCapacity = diskCapacity + self.diskPath = diskPath + self.cachePolicy = cachePolicy + } +} diff --git a/Sources/SwiftNetKit/NetworkService.swift b/Sources/SwiftNetKit/NetworkService.swift index 1a9eca1..2527677 100644 --- a/Sources/SwiftNetKit/NetworkService.swift +++ b/Sources/SwiftNetKit/NetworkService.swift @@ -22,6 +22,7 @@ public struct NetworkService: NetworkServiceProtocol { sessionConfiguration = URLSessionConfiguration.background(withIdentifier: identifier) } + // Default: 60.0s if let timeoutInterval = timeoutInterval { sessionConfiguration.timeoutIntervalForRequest = timeoutInterval sessionConfiguration.timeoutIntervalForResource = timeoutInterval @@ -30,18 +31,41 @@ public struct NetworkService: NetworkServiceProtocol { self.session = URLSession(configuration: sessionConfiguration) } + private func configureCache(for urlRequest: inout URLRequest, using request: any RequestProtocol) { + if let cacheConfig = request.cacheConfiguration { + let cache = URLCache( + memoryCapacity: cacheConfig.memoryCapacity, + diskCapacity: cacheConfig.diskCapacity, + diskPath: cacheConfig.diskPath + ) + + // Configure the URLSession's URLCache with the specified memory and disk capacity + self.session.configuration.urlCache = cache + + // Set the cache policy for the individual URLRequest, determining how the URLRequest uses the URLCache + urlRequest.cachePolicy = cacheConfig.cachePolicy + } else { + // If no custom cache configuration is provided for this request, + // then reset the session's URLCache to the system-wide default cache. + // This ensures that subsequent requests use the default caching behavior + // provided by URLCache.shared. + self.session.configuration.urlCache = URLCache.shared + } + } + func start( _ request: Request, retries: Int = 0, retryInterval: TimeInterval = 1.0 ) async throws -> Request.ResponseType { + var urlRequest = request.buildURLRequest() + self.configureCache(for: &urlRequest, using: request) + var currentAttempt = 0 var lastError: Error? while currentAttempt <= retries { do { - let urlRequest = request.buildURLRequest() - let (data, response) = try await session.data(for: urlRequest) guard let httpResponse = response as? HTTPURLResponse else { @@ -76,12 +100,13 @@ public struct NetworkService: NetworkServiceProtocol { retryInterval: TimeInterval = 1.0, completion: @escaping (Result) -> Void ) { + var urlRequest = request.buildURLRequest() + self.configureCache(for: &urlRequest, using: request) + var currentAttempt = 0 func attempt() { - let urlRequest = request.buildURLRequest() - - session.dataTask(with: urlRequest) { data, response, error in + self.session.dataTask(with: urlRequest) { data, response, error in if let error = error { if currentAttempt < retries { currentAttempt += 1 diff --git a/Sources/SwiftNetKit/Protocols/RequestProtocol.swift b/Sources/SwiftNetKit/Protocols/RequestProtocol.swift index fd4ae7a..7d55443 100644 --- a/Sources/SwiftNetKit/Protocols/RequestProtocol.swift +++ b/Sources/SwiftNetKit/Protocols/RequestProtocol.swift @@ -15,6 +15,7 @@ protocol RequestProtocol { var parameters: [String: Any]? { get } var headers: [String: String]? { get } var body: RequestBody? { get } + var cacheConfiguration: CacheConfiguration? { get } func buildURLRequest() -> URLRequest } @@ -23,6 +24,8 @@ extension RequestProtocol { func buildURLRequest() -> URLRequest { var urlRequest = URLRequest(url: self.url) + urlRequest.cachePolicy = self.cacheConfiguration?.cachePolicy ?? .useProtocolCachePolicy + if let parameters = self.parameters { let queryItems = parameters.map { key, value in URLQueryItem(name: key, value: "\(value)") diff --git a/Tests/SwiftNetKitTests/NetworkServiceTests.swift b/Tests/SwiftNetKitTests/NetworkServiceTests.swift index aec56b6..d35459b 100644 --- a/Tests/SwiftNetKitTests/NetworkServiceTests.swift +++ b/Tests/SwiftNetKitTests/NetworkServiceTests.swift @@ -114,6 +114,45 @@ final class NetworkServiceTests: XCTestCase { wait(for: [expectation], timeout: 5.0) } + + func testCachingBehavior() { + // Disclaimer: This test doesn't necessarily prove that the request was cached + + let expectation = XCTestExpectation(description: "Fetch data and cache it") + let cacheConfiguration = CacheConfiguration( + memoryCapacity: 10_000_000, + diskCapacity: 100_000_000, + cachePolicy: .returnCacheDataElseLoad + ) + + let firstRequest = BaseRequest( + url: self.getURL, + method: .get, + cacheConfiguration: cacheConfiguration + ) + + Task { + do { + let post: Post = try await self.networkService.start(firstRequest) + XCTAssertEqual(post.userId, 1) + XCTAssertEqual(post.id, 1) + + try await Task.sleep(nanoseconds: 1_000_000_000) + + let secondRequest = firstRequest + let cachedPost: Post = try await self.networkService.start(secondRequest) + + XCTAssertEqual(post.userId, cachedPost.userId) + XCTAssertEqual(post.id, cachedPost.id) + + expectation.fulfill() + } catch { + XCTFail("Failed with error: \(error)") + } + } + + wait(for: [expectation], timeout: 10.0) + } } // 'Post' for testing jsonplaceholder.typicode.com data