Gemini/GeminiProtocol/GeminiDataTask.swift

125 lines
3.8 KiB
Swift
Raw Normal View History

2020-07-15 02:39:38 +00:00
//
// GeminiDataTask.swift
// GeminiProtocol
//
// Created by Shadowfacts on 7/14/20.
//
import Foundation
import Network
public class GeminiDataTask {
public typealias Completion = (Result<GeminiResponse, Error>) -> Void
2020-07-15 02:39:38 +00:00
private static let queue = DispatchQueue(label: "GeminiDataTask", qos: .default)
2020-07-15 02:39:38 +00:00
public let request: GeminiRequest
private let completion: Completion
private var state = State.unstarted
private let connection: NWConnection
2021-09-29 00:20:08 +00:00
// todo: remove stupid hack when deployment target is >= iOS 15/macOS 12
private var _attribution: Any? = nil
#if os(iOS)
2021-09-29 00:20:08 +00:00
@available(iOS 15.0, *)
public var attribution: NWParameters.Attribution {
get {
_attribution as? NWParameters.Attribution ?? .developer
}
set {
_attribution = newValue
}
}
#endif
2020-07-15 02:39:38 +00:00
public init(request: GeminiRequest, completion: @escaping Completion) {
2020-07-15 02:39:38 +00:00
self.request = request
self.completion = completion
2020-07-23 13:18:59 +00:00
let port = request.url.port != nil ? UInt16(request.url.port!) : 1965
let endpoint: NWEndpoint = .hostPort(host: NWEndpoint.Host(request.url.host!), port: NWEndpoint.Port(rawValue: port)!)
self.connection = NWConnection(to: endpoint, using: .gemini)
self.connection.stateUpdateHandler = { (newState) in
switch newState {
case .ready:
self.sendRequest()
case let .failed(error):
2020-07-23 13:18:59 +00:00
self.state = .completed
self.connection.cancel()
self.completion(.failure(.connectionError(error)))
default:
break
}
}
2020-07-15 02:39:38 +00:00
}
public convenience init(url: URL, completion: @escaping Completion) throws {
2020-07-15 02:39:38 +00:00
self.init(request: try GeminiRequest(url: url), completion: completion)
}
deinit {
self.cancel()
}
public func resume() {
guard state == .unstarted else { return }
#if os(iOS)
if #available(iOS 15.0, *) {
connection.parameters.attribution = attribution
}
#endif
2020-07-23 13:18:59 +00:00
state = .started
connection.start(queue: GeminiDataTask.queue)
2020-07-15 02:39:38 +00:00
}
public func cancel() {
guard state != .completed else { return }
connection.cancel()
state = .completed
}
private func sendRequest() {
let message = NWProtocolFramer.Message(geminiRequest: request)
let context = NWConnection.ContentContext(identifier: "GeminiRequest", metadata: [message])
2020-07-23 13:18:59 +00:00
connection.send(content: nil, contentContext: context, isComplete: true, completion: .contentProcessed({ (error) in
if let error = error {
self.state = .completed
self.connection.cancel()
self.completion(.failure(.connectionError(error)))
}
}))
receive()
}
private func receive() {
connection.receiveMessage { (data, context, isComplete, error) in
if let error = error {
self.completion(.failure(.connectionError(error)))
} else if let message = context?.protocolMetadata(definition: GeminiProtocol.definition) as? NWProtocolFramer.Message,
let header = message.geminiResponseHeader {
2021-06-16 02:24:16 +00:00
guard isComplete else { fatalError() }
let response = GeminiResponse(header: header, body: data)
self.completion(.success(response))
}
self.connection.cancel()
self.state = .completed
}
}
}
extension GeminiDataTask {
enum State {
case unstarted, started, completed
2020-07-15 02:39:38 +00:00
}
}
public extension GeminiDataTask {
enum Error: Swift.Error {
case noData
case connectionError(NWError)
}
}