From 94e0bdba5a26f8ee56b4cde43a521e84ec96e67d Mon Sep 17 00:00:00 2001 From: Logan Sease Date: Thu, 30 Oct 2025 13:09:58 -0400 Subject: [PATCH] fix memory issues --- .../Calls/NetworkingClient+Requests.swift | 4 +-- Sources/Networking/NetworkingClient.swift | 9 ++++++ Sources/Networking/NetworkingRequest.swift | 28 +++++++++---------- Tests/NetworkingTests/NetworkingTests.swift | 5 ++-- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/Sources/Networking/Calls/NetworkingClient+Requests.swift b/Sources/Networking/Calls/NetworkingClient+Requests.swift index bf4a56d..fcd4201 100644 --- a/Sources/Networking/Calls/NetworkingClient+Requests.swift +++ b/Sources/Networking/Calls/NetworkingClient+Requests.swift @@ -31,7 +31,7 @@ public extension NetworkingClient { } internal func request(_ httpMethod: HTTPMethod, _ route: String, params: Params = Params(), data: Data? = nil) -> NetworkingRequest { - let req = NetworkingRequest(logger: self.logger) + let req = NetworkingRequest(logger: self.logger, urlSession: self.urlSession) req.httpMethod = httpMethod req.route = route req.params = params @@ -43,9 +43,7 @@ public extension NetworkingClient { req?.logLevel = self.logLevel req?.headers = self.headers req?.parameterEncoding = self.parameterEncoding - req?.sessionConfiguration = self.sessionConfiguration req?.timeout = self.timeout - req?.sessionDelegate = self.sessionDelegate ?? nil } updateRequest() req.requestRetrier = { [weak self] in diff --git a/Sources/Networking/NetworkingClient.swift b/Sources/Networking/NetworkingClient.swift index d555cfa..f246eb5 100644 --- a/Sources/Networking/NetworkingClient.swift +++ b/Sources/Networking/NetworkingClient.swift @@ -30,6 +30,15 @@ public class NetworkingClient { } internal let logger: NetworkingLogger + + // URLSession is created lazily and reused for all requests from this client + // The session is shared by reference with all NetworkingRequest instances, + // so it remains alive as long as any requests are using it + internal lazy var urlSession: URLSession = { + let config = sessionConfiguration + let delegate = sessionDelegate + return URLSession(configuration: config, delegate: delegate, delegateQueue: nil) + }() public init(baseURL: String, timeout: TimeInterval? = nil, filteredWords: [String] = []) { self.baseURL = baseURL diff --git a/Sources/Networking/NetworkingRequest.swift b/Sources/Networking/NetworkingRequest.swift index 497ef19..62434d3 100644 --- a/Sources/Networking/NetworkingRequest.swift +++ b/Sources/Networking/NetworkingRequest.swift @@ -28,14 +28,14 @@ public class NetworkingRequest: NSObject { private let logger: NetworkingLogger var timeout: TimeInterval? let progressPublisher = PassthroughSubject() - var sessionConfiguration: URLSessionConfiguration? - var sessionDelegate: URLSessionDelegate? + private let urlSession: URLSession var requestRetrier: NetworkRequestRetrier? var asyncRequestRetrier: NetworkRequestRetrierAsync? private let maxRetryCount = 3 - init(logger: NetworkingLogger = NetworkingLogger()) { + init(logger: NetworkingLogger = NetworkingLogger(), urlSession: URLSession = URLSession.shared) { self.logger = logger + self.urlSession = urlSession } public func uploadPublisher() -> AnyPublisher<(Data?, Progress), Error> { @@ -46,10 +46,9 @@ public class NetworkingRequest: NSObject { } logger.log(request: urlRequest) - let config = sessionConfiguration ?? URLSessionConfiguration.default - let sessionDelegate = sessionDelegate ?? self - let urlSession = URLSession(configuration: config, delegate: sessionDelegate, delegateQueue: nil) - let callPublisher: AnyPublisher<(Data?, Progress), Error> = urlSession.dataTaskPublisher(for: urlRequest) + // For upload progress tracking, we need a dedicated session with this request as delegate + let uploadSession = URLSession(configuration: urlSession.configuration, delegate: self, delegateQueue: nil) + let callPublisher: AnyPublisher<(Data?, Progress), Error> = uploadSession.dataTaskPublisher(for: urlRequest) .tryMap { (data: Data, response: URLResponse) -> Data in self.logger.log(response: response, data: data) if let httpURLResponse = response as? HTTPURLResponse { @@ -66,7 +65,14 @@ public class NetworkingRequest: NSObject { return NetworkingError(error: error) }.map { data -> (Data?, Progress) in return (data, Progress()) - }.eraseToAnyPublisher() + }.handleEvents(receiveCompletion: { _ in + // Clean up the upload session after completion + uploadSession.finishTasksAndInvalidate() + }, receiveCancel: { + // Clean up the upload session if cancelled + uploadSession.invalidateAndCancel() + }) + .eraseToAnyPublisher() let progressPublisher2: AnyPublisher<(Data?, Progress), Error> = progressPublisher .map { progress -> (Data?, Progress) in @@ -88,9 +94,6 @@ public class NetworkingRequest: NSObject { } logger.log(request: urlRequest) - let config = sessionConfiguration ?? URLSessionConfiguration.default - let sessionDelegate = sessionDelegate ?? self - let urlSession = URLSession(configuration: config, delegate: sessionDelegate, delegateQueue: nil) return urlSession.dataTaskPublisher(for: urlRequest) .tryMap { (data: Data, response: URLResponse) -> Data in self.logger.log(response: response, data: data) @@ -131,9 +134,6 @@ public class NetworkingRequest: NSObject { throw NetworkingError.unableToParseRequest } logger.log(request: urlRequest) - let config = sessionConfiguration ?? URLSessionConfiguration.default - let sessionDelegate = sessionDelegate ?? self - let urlSession = URLSession(configuration: config, delegate: sessionDelegate, delegateQueue: nil) let (data, urlResponse) = try await urlSession.data(for: urlRequest) if let httpResponse = urlResponse as? HTTPURLResponse, diff --git a/Tests/NetworkingTests/NetworkingTests.swift b/Tests/NetworkingTests/NetworkingTests.swift index cc55e8f..29e3599 100644 --- a/Tests/NetworkingTests/NetworkingTests.swift +++ b/Tests/NetworkingTests/NetworkingTests.swift @@ -9,8 +9,8 @@ final class NetworkingTests: XCTestCase { func testBadURLDoesntCrash() { let exp = expectation(description: "call") - let client = NetworkingClient(baseURL: "https://jsonplaceholder.typicode.com") - client.get("/forge a bad url") + let client = NetworkingClient(baseURL: "ht!tp://invalid url with spaces") + client.get("/users") .sink(receiveCompletion: { completion in switch completion { case .finished: @@ -24,6 +24,7 @@ final class NetworkingTests: XCTestCase { receiveValue: { (json: Any) in print(json) }).store(in: &cancellables) + waitForExpectations(timeout: 1, handler: nil) } }