Merge GeminiConnection into GeminiDataTask

This commit is contained in:
Shadowfacts 2020-07-22 18:25:56 -04:00
parent 70bbbc5583
commit 06a9c1fcac
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
5 changed files with 61 additions and 132 deletions

View File

@ -17,7 +17,6 @@
D626648D24BBF22E00DF9B88 /* GeminiProtocol.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D626647724BBF22E00DF9B88 /* GeminiProtocol.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D626648D24BBF22E00DF9B88 /* GeminiProtocol.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D626647724BBF22E00DF9B88 /* GeminiProtocol.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
D626649C24BBF24100DF9B88 /* GeminiResponseHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649524BBF24000DF9B88 /* GeminiResponseHeader.swift */; }; D626649C24BBF24100DF9B88 /* GeminiResponseHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649524BBF24000DF9B88 /* GeminiResponseHeader.swift */; };
D626649D24BBF24100DF9B88 /* Message+Gemini.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649624BBF24100DF9B88 /* Message+Gemini.swift */; }; D626649D24BBF24100DF9B88 /* Message+Gemini.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649624BBF24100DF9B88 /* Message+Gemini.swift */; };
D626649E24BBF24100DF9B88 /* GeminiConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649724BBF24100DF9B88 /* GeminiConnection.swift */; };
D626649F24BBF24100DF9B88 /* NWParameters+Gemini.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649824BBF24100DF9B88 /* NWParameters+Gemini.swift */; }; D626649F24BBF24100DF9B88 /* NWParameters+Gemini.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649824BBF24100DF9B88 /* NWParameters+Gemini.swift */; };
D62664A024BBF24100DF9B88 /* GeminiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649924BBF24100DF9B88 /* GeminiProtocol.swift */; }; D62664A024BBF24100DF9B88 /* GeminiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649924BBF24100DF9B88 /* GeminiProtocol.swift */; };
D62664A124BBF24100DF9B88 /* GeminiRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649A24BBF24100DF9B88 /* GeminiRequest.swift */; }; D62664A124BBF24100DF9B88 /* GeminiRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626649A24BBF24100DF9B88 /* GeminiRequest.swift */; };
@ -250,7 +249,6 @@
D626648824BBF22E00DF9B88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; D626648824BBF22E00DF9B88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D626649524BBF24000DF9B88 /* GeminiResponseHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiResponseHeader.swift; sourceTree = "<group>"; }; D626649524BBF24000DF9B88 /* GeminiResponseHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiResponseHeader.swift; sourceTree = "<group>"; };
D626649624BBF24100DF9B88 /* Message+Gemini.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Message+Gemini.swift"; sourceTree = "<group>"; }; D626649624BBF24100DF9B88 /* Message+Gemini.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Message+Gemini.swift"; sourceTree = "<group>"; };
D626649724BBF24100DF9B88 /* GeminiConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiConnection.swift; sourceTree = "<group>"; };
D626649824BBF24100DF9B88 /* NWParameters+Gemini.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NWParameters+Gemini.swift"; sourceTree = "<group>"; }; D626649824BBF24100DF9B88 /* NWParameters+Gemini.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NWParameters+Gemini.swift"; sourceTree = "<group>"; };
D626649924BBF24100DF9B88 /* GeminiProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiProtocol.swift; sourceTree = "<group>"; }; D626649924BBF24100DF9B88 /* GeminiProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiProtocol.swift; sourceTree = "<group>"; };
D626649A24BBF24100DF9B88 /* GeminiRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiRequest.swift; sourceTree = "<group>"; }; D626649A24BBF24100DF9B88 /* GeminiRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeminiRequest.swift; sourceTree = "<group>"; };
@ -454,7 +452,6 @@
D626649A24BBF24100DF9B88 /* GeminiRequest.swift */, D626649A24BBF24100DF9B88 /* GeminiRequest.swift */,
D626649524BBF24000DF9B88 /* GeminiResponseHeader.swift */, D626649524BBF24000DF9B88 /* GeminiResponseHeader.swift */,
D69F00AD24BEA29100E37622 /* GeminiResponse.swift */, D69F00AD24BEA29100E37622 /* GeminiResponse.swift */,
D626649724BBF24100DF9B88 /* GeminiConnection.swift */,
D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */, D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */,
); );
path = GeminiProtocol; path = GeminiProtocol;
@ -965,7 +962,6 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D62664A024BBF24100DF9B88 /* GeminiProtocol.swift in Sources */, D62664A024BBF24100DF9B88 /* GeminiProtocol.swift in Sources */,
D626649E24BBF24100DF9B88 /* GeminiConnection.swift in Sources */,
D69F00AC24BE9DD300E37622 /* GeminiDataTask.swift in Sources */, D69F00AC24BE9DD300E37622 /* GeminiDataTask.swift in Sources */,
D62664A124BBF24100DF9B88 /* GeminiRequest.swift in Sources */, D62664A124BBF24100DF9B88 /* GeminiRequest.swift in Sources */,
D626649F24BBF24100DF9B88 /* NWParameters+Gemini.swift in Sources */, D626649F24BBF24100DF9B88 /* NWParameters+Gemini.swift in Sources */,

View File

@ -7,12 +7,12 @@
<key>BrowserCore.xcscheme_^#shared#^_</key> <key>BrowserCore.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>5</integer> <integer>2</integer>
</dict> </dict>
<key>Gemini-iOS.xcscheme_^#shared#^_</key> <key>Gemini-iOS.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>2</integer> <integer>3</integer>
</dict> </dict>
<key>Gemini.xcscheme_^#shared#^_</key> <key>Gemini.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -27,12 +27,12 @@
<key>GeminiProtocol.xcscheme_^#shared#^_</key> <key>GeminiProtocol.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>3</integer> <integer>4</integer>
</dict> </dict>
<key>GeminiRenderer.xcscheme_^#shared#^_</key> <key>GeminiRenderer.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>4</integer> <integer>5</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>

View File

@ -27,7 +27,7 @@ class BrowserWindowController: NSWindowController {
override func windowDidLoad() { override func windowDidLoad() {
super.windowDidLoad() super.windowDidLoad()
contentViewController = NSHostingController(rootView: BrowserView(navigator: navigator)) contentViewController = NSHostingController(rootView: ContentView(navigator: navigator))
urlUpdater = navigator.$currentURL.sink(receiveValue: self.currentURLChanged(_:)) urlUpdater = navigator.$currentURL.sink(receiveValue: self.currentURLChanged(_:))
} }

View File

@ -1,79 +0,0 @@
//
// GeminiConnection.swift
// Gemini
//
// Created by Shadowfacts on 7/12/20.
//
import Foundation
import Network
public protocol GeminiConnectionDelegate: class {
func connectionReady(_ connection: GeminiConnection)
func connection(_ connection: GeminiConnection, receivedData data: Data?, header: GeminiResponseHeader)
func connection(_ connection: GeminiConnection, handleError error: GeminiConnection.Error)
func connectionCompleted(_ connection: GeminiConnection)
}
public class GeminiConnection {
public typealias Error = NWError
public weak var delegate: GeminiConnectionDelegate?
private let connection: NWConnection
public init(endpoint: NWEndpoint, delegate: GeminiConnectionDelegate? = nil) {
self.connection = NWConnection(to: endpoint, using: .gemini)
self.delegate = delegate
}
public func startConnection() {
connection.stateUpdateHandler = { (newState) in
switch newState {
case .ready:
print("\(self.connection) established")
self.delegate?.connectionReady(self)
case let .failed(error):
print("\(self.connection) failed: \(error)")
self.connection.cancel()
default:
break
}
}
// todo: should this be on a background queue?
connection.start(queue: .main)
}
public func cancelConnection() {
if connection.state != .cancelled {
connection.cancel()
}
}
public func sendRequest(_ request: GeminiRequest) {
let message = NWProtocolFramer.Message(geminiRequest: request)
let context = NWConnection.ContentContext(identifier: "GeminiRequest", metadata: [message])
// todo: should this really always be idempotent?
connection.send(content: nil, contentContext: context, isComplete: true, completion: .contentProcessed({ (_) in }))
receiveNextMessage()
}
private func receiveNextMessage() {
connection.receiveMessage { (data, context, isComplete, error) in
if let error = error {
self.delegate?.connection(self, handleError: error)
} else if let message = context?.protocolMetadata(definition: GeminiProtocol.definition) as? NWProtocolFramer.Message,
let header = message.geminiResponseHeader,
let delegate = self.delegate {
delegate.connection(self, receivedData: data, header: header)
}
guard isComplete else { fatalError("Connection should complete immediately") }
self.delegate?.connectionCompleted(self)
self.connection.cancel()
}
}
}

View File

@ -9,22 +9,35 @@ import Foundation
import Network import Network
public class GeminiDataTask { public class GeminiDataTask {
public typealias Completion = (Result<GeminiResponse, Error>) -> Void
private static let queue = DispatchQueue(label: "GeminiDataTask", qos: .default)
public let request: GeminiRequest public let request: GeminiRequest
private let completion: (Result<GeminiResponse, Error>) -> Void private let completion: Completion
private var state = State.unstarted
private let connection: NWConnection
private let connection: GeminiConnection public init(request: GeminiRequest, completion: @escaping Completion) {
public private(set) var completed: Bool = false
public init(request: GeminiRequest, completion: @escaping (Result<GeminiResponse, Error>) -> Void) {
self.request = request self.request = request
self.completion = completion self.completion = completion
self.connection = GeminiConnection(endpoint: .url(request.url)) self.connection = NWConnection(to: .url(request.url), using: .gemini)
self.connection.delegate = self
self.connection.stateUpdateHandler = { (newState) in
switch newState {
case .ready:
self.sendRequest()
case let .failed(error):
self.connection.cancel()
self.completion(.failure(.connectionError(error)))
default:
break
}
}
} }
public convenience init(url: URL, completion: @escaping (Result<GeminiResponse, Error>) -> Void) throws { public convenience init(url: URL, completion: @escaping Completion) throws {
self.init(request: try GeminiRequest(url: url), completion: completion) self.init(request: try GeminiRequest(url: url), completion: completion)
} }
@ -33,11 +46,43 @@ public class GeminiDataTask {
} }
public func resume() { public func resume() {
self.connection.startConnection() guard state == .unstarted else { return }
connection.start(queue: GeminiDataTask.queue)
} }
public func cancel() { public func cancel() {
connection.cancelConnection() 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])
connection.send(content: nil, contentContext: context, isComplete: true, completion: .contentProcessed({ (_) in }))
state = .started
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 {
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
} }
} }
@ -47,36 +92,3 @@ public extension GeminiDataTask {
case connectionError(NWError) case connectionError(NWError)
} }
} }
extension GeminiDataTask: GeminiConnectionDelegate {
public func connectionReady(_ connection: GeminiConnection) {
connection.sendRequest(self.request)
}
public func connection(_ connection: GeminiConnection, receivedData data: Data?, header: GeminiResponseHeader) {
guard !completed else {
print("GeminiRequestTask(\(self)) has already completed, shouldn't be receiving any data")
return
}
completed = true
if let data = data {
let response = GeminiResponse(header: header, body: data)
self.completion(.success(response))
} else {
self.completion(.failure(.noData))
}
}
public func connection(_ connection: GeminiConnection, handleError error: GeminiConnection.Error) {
guard !completed else {
print("GeminiRequestTask(\(self)) has already completed, shouldn't be receiving any data")
return
}
completed = true
self.completion(.failure(.connectionError(error)))
}
public func connectionCompleted(_ connection: GeminiConnection) {
}
}