p2p-cljs / src / ird / p2p / transport.cljs
535 words
 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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
(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"})

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/p2p-cljs.git