1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
(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<String, dynamic>
;; where it wants a Stream<String>, 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<String, dynamic> where it wants a
;; Stream<String> 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)))
|