diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index c491ca0..06913bf 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -29,3 +29,14 @@ jobs: - name: Build and test run: xcodebuild test -scheme OllamaKit -destination 'platform=macOS,arch=x86_64' + + test-linux: + runs-on: ubuntu-latest + steps: + - name: Swift version + run: swift --version + + - uses: actions/checkout@v4 + + - name: Build and test + run: swift build \ No newline at end of file diff --git a/Sources/OllamaKit/Utils/OKHTTPClient.swift b/Sources/OllamaKit/Utils/OKHTTPClient.swift index 85c702d..088e224 100644 --- a/Sources/OllamaKit/Utils/OKHTTPClient.swift +++ b/Sources/OllamaKit/Utils/OKHTTPClient.swift @@ -5,8 +5,10 @@ // Created by Kevin Hermawan on 08/06/24. // -import Combine import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif internal struct OKHTTPClient { private let decoder: JSONDecoder = .default @@ -26,7 +28,53 @@ internal extension OKHTTPClient { return try decoder.decode(T.self, from: data) } - + + #if canImport(FoundationNetworking) + func stream(request: URLRequest, with responseType: T.Type) -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in + Task { + let delegate = StreamingDelegate( + urlResponseCallback: { response in + do { + try validate(response: response) + } catch { + continuation.finish(throwing: error) + } + }, + dataCallback: { buffer in // + while let chunk = self.extractNextJSON(from: &buffer) { + do { + let decodedObject = try self.decoder.decode(T.self, from: chunk) + continuation.yield(decodedObject) + } catch { + continuation.finish(throwing: error) + return + } + } + }, + completionCallback: { error in + if let error { + continuation.finish(throwing: error) + } + continuation.finish() + } + ) + + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: .main) + let task = session.dataTask(with: request) + + continuation.onTermination = { terminationState in + // Cancellation of our task should cancel the URLSessionDataTask + if case .cancelled = terminationState { + task.cancel() + } + } + + task.resume() + } + } + } + #else func stream(request: URLRequest, with responseType: T.Type) -> AsyncThrowingStream { return AsyncThrowingStream { continuation in Task { @@ -64,8 +112,12 @@ internal extension OKHTTPClient { } } } + #endif } +#if canImport(Combine) +import Combine + internal extension OKHTTPClient { func send(request: URLRequest, with responseType: T.Type) -> AnyPublisher { return URLSession.shared.dataTaskPublisher(for: request) @@ -119,6 +171,7 @@ internal extension OKHTTPClient { .eraseToAnyPublisher() } } +#endif private extension OKHTTPClient { func validate(response: URLResponse) throws { diff --git a/Sources/OllamaKit/Utils/OKRouter.swift b/Sources/OllamaKit/Utils/OKRouter.swift index 0e3d280..e5eb885 100644 --- a/Sources/OllamaKit/Utils/OKRouter.swift +++ b/Sources/OllamaKit/Utils/OKRouter.swift @@ -6,6 +6,9 @@ // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif internal enum OKRouter { case root diff --git a/Sources/OllamaKit/Utils/StreamingDelegate.swift b/Sources/OllamaKit/Utils/StreamingDelegate.swift index a0ea39d..9153923 100644 --- a/Sources/OllamaKit/Utils/StreamingDelegate.swift +++ b/Sources/OllamaKit/Utils/StreamingDelegate.swift @@ -5,8 +5,9 @@ // Created by Kevin Hermawan on 09/06/24. // -@preconcurrency import Combine import Foundation +#if canImport(Combine) +@preconcurrency import Combine internal class StreamingDelegate: NSObject, URLSessionDataDelegate, @unchecked Sendable { private let subject = PassthroughSubject() @@ -27,3 +28,52 @@ internal class StreamingDelegate: NSObject, URLSessionDataDelegate, @unchecked S subject.eraseToAnyPublisher() } } +#endif + +#if canImport(FoundationNetworking) +import FoundationNetworking +internal final class StreamingDelegate: NSObject, URLSessionDataDelegate, @unchecked Sendable { + + let urlResponseCallback: (@Sendable (URLResponse) -> Void)? + let dataCallback: (@Sendable (inout Data) -> Void)? + let completionCallback: (@Sendable (Error?) -> Void)? + + var buffer = Data() + + init(urlResponseCallback: (@Sendable (URLResponse) -> Void)?, + dataCallback: (@Sendable (inout Data) -> Void)?, + completionCallback: (@Sendable (Error?) -> Void)? + ) { + self.urlResponseCallback = urlResponseCallback + self.dataCallback = dataCallback + self.completionCallback = completionCallback + } + + func urlSession( + _ session: URLSession, + dataTask: URLSessionDataTask, + didReceive response: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void + ) { + // Handle the URLResponse here + urlResponseCallback?(response) + completionHandler(.allow) + } + + func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + didReceive data: Data) { + buffer.append(data) + + // Handle the incoming data chunk here + dataCallback?(&buffer) + } + + func urlSession(_ session: URLSession, + task: URLSessionTask, + didCompleteWithError error: Error?) { + // Handle completion or errors + completionCallback?(error) + } +} +#endif