(ns frq.net.dart "An IRC socket over dart:io. The thing the jolt APK could never have. `frq.irc` reaches OpenSSL through the dynamic loader for TLS, and Android ships no public libssl — so on the phone `:6697` was unreachable, sign-in was desktop-only, and the connect screen fell back to the plain `:6667` listener on its own. `SecureSocket` is in the Dart runtime. There is nothing to load and nothing to find. The shape is different from the desktop's and deliberately so. jolt has one future blocking on a read, which also has to flush the outbox because the reader owns the connection; Dart has an event loop, so a socket is a `Stream` and a write is a write. No thread, no outbox, no poll interval. What comes out is `frq.irc.parse/parse-line`'s maps — the same parser the desktop runs, out of common/." (:require ["dart:convert" :as conv] ["dart:io" :as io] [frq.irc.parse :as parse])) (defn ^:async connect! "Open a connection and start reading it. `on-msg` is called with a parsed map per line; `on-close` with a reason, or nil where the peer simply went away. Returns the socket, which `send-line!` and `close!` take." [{:keys [host port tls? on-msg on-close]}] (let [buffer (atom "") sock (await (if tls? (io/SecureSocket.connect host port) (io/Socket.connect host port)))] ;; Lines are cut here rather than by a LineSplitter in the chain. Two ;; transforms deep, cljd hands the splitter a CastStream ;; where it wants a Stream, and it throws at the first byte — the ;; generic is lost through the dynamic call. Buffering the decoded chunks ;; is a few lines and has no type to lose. ;; ;; \r\n or \n: IRC says the former and servers send both. (-> sock (.transform (.-decoder conv/utf8)) (.listen (fn [chunk] ;; Split rather than a LineSplitter in the chain: two ;; transforms deep, cljd hands the splitter a ;; CastStream where it wants a ;; Stream and it throws at the first byte — the ;; generic is lost through the dynamic call. ;; ;; The tail after the last newline is a partial line and ;; goes back in the buffer. A chunk boundary falls wherever ;; TCP puts it, which is not where IRC put its lines. (let [parts (.split (str @buffer chunk) "\n") whole (butlast parts)] (reset! buffer (str (last parts))) (doseq [raw whole] (let [line (.trim (str raw))] (when (and on-msg (pos? (count line))) (let [m (parse/parse-line line)] ;; PING is answered here rather than upstairs: a ;; client that leaves it to the reducer is one ;; queue away from a timeout, and nothing about ;; the answer is a decision. (when (= "PING" (:command m)) (.write sock (str "PONG :" (first (:params m)) "\r\n"))) (on-msg m))))))) .onDone (fn [] (when on-close (on-close nil))) .onError (fn [e _] (when on-close (on-close (str e)))) .cancelOnError true)) sock)) (defn send-line! [sock line] (.write sock (str line "\r\n"))) (defn close! [sock] (try (.destroy sock) (catch Exception _ nil)))