damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

WebSocket.swift (3099B)


      1 //
      2 //  WebSocket.swift
      3 //  damus
      4 //
      5 //  Created by Bryan Montz on 4/13/23.
      6 //
      7 
      8 import Combine
      9 import Foundation
     10 
     11 enum WebSocketEvent {
     12     case connected
     13     case message(URLSessionWebSocketTask.Message)
     14     case disconnected(URLSessionWebSocketTask.CloseCode, String?)
     15     case error(Error)
     16     
     17     var description: String? {
     18         switch self {
     19         case .connected:
     20             return "Connected"
     21         case .message:
     22             return nil  // adding this to the RelayLog was too noisy
     23         case .disconnected(let close_code, let reason):
     24             return "Disconnected: Close code: \(close_code), reason: \(reason ?? "unknown")"
     25         case .error(let error):
     26             return "Error: \(error)"
     27         }
     28     }
     29 }
     30 
     31 final class WebSocket: NSObject, URLSessionWebSocketDelegate {
     32     
     33     private let url: URL
     34     private let session: URLSession
     35     private lazy var webSocketTask: URLSessionWebSocketTask = {
     36         let task = session.webSocketTask(with: url)
     37         task.delegate = self
     38         return task
     39     }()
     40     
     41     let subject = PassthroughSubject<WebSocketEvent, Never>()
     42     
     43     init(_ url: URL, session: URLSession = .shared) {
     44         self.url = url
     45         self.session = session
     46     }
     47     
     48     func ping(receiveHandler: @escaping (Error?) -> Void) {
     49         self.webSocketTask.sendPing(pongReceiveHandler: receiveHandler)
     50     }
     51     
     52     func connect() {
     53         resume()
     54     }
     55     
     56     func disconnect(closeCode: URLSessionWebSocketTask.CloseCode = .normalClosure, reason: Data? = nil) {
     57         webSocketTask.cancel(with: closeCode, reason: reason)
     58         
     59         // reset after disconnecting to be ready for reconnecting
     60         let task = session.webSocketTask(with: url)
     61         task.delegate = self
     62         webSocketTask = task
     63         
     64         let reason_str: String?
     65         if let reason {
     66             reason_str = String(data: reason, encoding: .utf8)
     67         } else {
     68             reason_str = nil
     69         }
     70         subject.send(.disconnected(closeCode, reason_str))
     71     }
     72     
     73     func send(_ message: URLSessionWebSocketTask.Message) {
     74         webSocketTask.send(message) { [weak self] error in
     75             if let error {
     76                 self?.subject.send(.error(error))
     77             }
     78         }
     79     }
     80     
     81     private func resume() {
     82         webSocketTask.receive { [weak self] result in
     83             switch result {
     84             case .success(let message):
     85                 self?.subject.send(.message(message))
     86                 self?.resume()
     87             case .failure(let error):
     88                 self?.subject.send(.error(error))
     89             }
     90         }
     91         
     92         webSocketTask.resume()
     93     }
     94     
     95     // MARK: - URLSessionWebSocketDelegate
     96     
     97     func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol theProtocol: String?) {
     98         subject.send(.connected)
     99     }
    100     
    101     func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
    102         disconnect(closeCode: closeCode, reason: reason)
    103     }
    104 }