(ns ird.p2p.transport
"The relay transport SPI: one room session over the app's signaling
service, in whatever protocol carries it (the node's `/ws` relay in
ird.p2p.transport.ws, gossipsub over libp2p in ird.p2p.transport.libp2p).
The room logic only ever sees plain frame maps and channels.
Client frames (what a session sends):
{:t :join :room <id> :peer <id>} (sent by the session itself)
{:t :signal :to <peer> :payload <envelope map>}
{:t :leave} {:t :ping}
Server frames (what a session delivers on its inbound channel):
{:t :peers :peers [<peer> ...]} the roster answered to our join
{:t :peer-joined :peer <id>} {:t :peer-left :peer <id>}
{:t :signal :from <peer> :payload <value>} `from` stamped by the relay
{:t :pong} {:t :error :code <string>}
unknown types are kept as {:t <keyword> ...}
plus the session's own lifecycle frames:
{:t :joined} {:t :reconnecting :attempt <n> :after-ms <ms>} {:t :closed :reason <string>}
Wire JSON uses the service's strings (\"peer-joined\", ...); `encode-frame`
and `decode-frame` convert."
(:require [clojure.string :as str]
[ird.p2p.crypto :as crypto]
[ird.p2p.impl.json :as json]))
(defprotocol Transport
(connect [transport opts]
"Join a room. OPTS: :room (id), :peer (this peer's id), :url, :origin,
:limits (the descriptor's), plus transport-specific options. Channel of
a Session once joined, or an error value when the join is refused for
good (:ird.p2p/upgrade-rejected 403/404, :ird.p2p/relay-error room-full /
too-many-rooms / bad-message)."))
(defprotocol Session
(send! [session frame]
"Send a client frame map. Channel of true, or an error value
(:ird.p2p/frame-too-large, :ird.p2p/not-joined, :ird.p2p/closed,
:ird.p2p/transport-error).")
(inbound [session]
"The channel of server and lifecycle frames; closes after {:t :closed}.")
(state [session]
"One of :connecting :joined :reconnecting :closed.")
(wake! [session]
"The app came to the foreground or the network changed: reconnect now
if not joined.")
(close! [session]
"Leave the room and close; the session cannot be restarted. Channel
that closes when done."))
;; ---- frame codec -----------------------------------------------------------------
(def ^:private type-names
{:join "join" :signal "signal" :leave "leave" :ping "ping"
:peers "peers" :peer-joined "peer-joined" :peer-left "peer-left" :pong "pong" :error "error"})
(defn encode-frame
"The wire text of a client frame, keys in the documented order."
[{:keys [t] :as frame}]
(json/stringify
(case t
:join {:t "join" :room (:room frame) :peer (:peer frame)}
:signal {:t "signal" :to (:to frame) :payload (:payload frame)}
:leave {:t "leave"}
:ping {:t "ping"}
(assoc (dissoc frame :t) :t (get type-names t (if (keyword? t) (name t) (str t)))))))
(defn- peer [v] (when (crypto/valid-peer-id? v) v))
(defn decode-frame
"A server frame map from a parsed JSON value; nil when it is not a frame
(no `t`, malformed peer ids). Unknown types decode to {:t <keyword> ...}."
[v]
(when (and (map? v) (string? (:t v)))
(case (:t v)
"peers" (let [ps (:peers v)]
(when (or (nil? ps) (vector? ps))
(let [ids (mapv peer ps)]
(when (every? some? ids) {:t :peers :peers ids}))))
"peer-joined" (when-let [p (peer (:peer v))] {:t :peer-joined :peer p})
"peer-left" (when-let [p (peer (:peer v))] {:t :peer-left :peer p})
"signal" (when-let [p (peer (:from v))]
(when (contains? v :payload) {:t :signal :from p :payload (:payload v)}))
"pong" {:t :pong}
"error" {:t :error :code (if (string? (:code v)) (:code v) "")}
(assoc v :t (keyword (:t v))))))
(defn decode-text
"A server frame from wire text; nil when undecodable (ignored for forward
compatibility)."
[text]
(decode-frame (json/parse text)))
(def terminal-relay-codes
"Relay error codes that end a session for good."
#{"room-full" "too-many-rooms" "bad-message"})