Skip to main content

netget/server/nostr/
subscriptions.rs

1//! Open subscriptions, per connection and across the relay.
2//!
3//! This is not storage. It holds only what NIP-01 makes a relay responsible for while a
4//! subscription is open — its id and filters — so that `CLOSE` needs no model call, so that the
5//! events the model supplies can be held to the filters that asked for them, and so that an
6//! event the model accepts reaches every other subscriber whose filters match it. Nothing is
7//! kept after a subscription closes, and no event is kept at all.
8
9use super::wire::{self, Event, Filter};
10use std::collections::HashMap;
11use std::sync::{Arc, Mutex};
12use tokio::sync::mpsc;
13
14/// What the connection's single writer sends.
15#[derive(Debug)]
16pub enum Outbound {
17    /// One relay message (a JSON array) as a text frame.
18    Text(String),
19    /// A close frame; the writer sends nothing after it.
20    Close { code: u16, reason: String },
21    /// The idle keepalive.
22    Ping(Vec<u8>),
23    /// The connection is over: flush what was queued before this and stop.
24    Shutdown,
25}
26
27#[derive(Debug)]
28struct Subscription {
29    filters: Vec<Filter>,
30    generation: u64,
31}
32
33#[derive(Debug, Default)]
34struct Subscriptions {
35    open: HashMap<String, Subscription>,
36    next_generation: u64,
37    /// The REQ the model is answering right now, if any: its id and generation.
38    answering: Option<(String, u64)>,
39}
40
41/// One connection's subscriptions and the sender its frames go through.
42#[derive(Debug)]
43pub struct ConnShared {
44    pub connection_id: u32,
45    out_tx: mpsc::UnboundedSender<Outbound>,
46    subs: Mutex<Subscriptions>,
47}
48
49impl ConnShared {
50    pub fn new(connection_id: u32, out_tx: mpsc::UnboundedSender<Outbound>) -> Self {
51        Self {
52            connection_id,
53            out_tx,
54            subs: Mutex::new(Subscriptions::default()),
55        }
56    }
57
58    fn lock(&self) -> std::sync::MutexGuard<'_, Subscriptions> {
59        self.subs
60            .lock()
61            .unwrap_or_else(|poisoned| poisoned.into_inner())
62    }
63
64    /// Queue one relay message.
65    pub fn send(&self, message: String) {
66        let _ = self.out_tx.send(Outbound::Text(message));
67    }
68
69    pub fn out_tx(&self) -> &mpsc::UnboundedSender<Outbound> {
70        &self.out_tx
71    }
72
73    /// Open (or, NIP-01, overwrite) a subscription. Returns its generation, or the `CLOSED`
74    /// message when the connection already holds [`wire::MAX_SUBSCRIPTIONS`].
75    pub fn open(&self, subscription_id: &str, filters: Vec<Filter>) -> Result<u64, String> {
76        let mut subs = self.lock();
77        if !subs.open.contains_key(subscription_id) && subs.open.len() >= wire::MAX_SUBSCRIPTIONS {
78            return Err(format!(
79                "rate-limited: at most {} open subscriptions per connection; CLOSE one first",
80                wire::MAX_SUBSCRIPTIONS
81            ));
82        }
83        subs.next_generation += 1;
84        let generation = subs.next_generation;
85        subs.open.insert(
86            subscription_id.to_string(),
87            Subscription {
88                filters,
89                generation,
90            },
91        );
92        Ok(generation)
93    }
94
95    /// Close a subscription. Whether it was open.
96    pub fn close(&self, subscription_id: &str) -> bool {
97        self.lock().open.remove(subscription_id).is_some()
98    }
99
100    /// Whether this exact subscription — not a later REQ reusing its id — is still open.
101    pub fn is_open(&self, subscription_id: &str, generation: u64) -> bool {
102        self.lock()
103            .open
104            .get(subscription_id)
105            .is_some_and(|s| s.generation == generation)
106    }
107
108    pub fn open_count(&self) -> usize {
109        self.lock().open.len()
110    }
111
112    /// Mark the REQ whose answer the model is producing.
113    pub fn begin_answer(&self, subscription_id: &str, generation: u64) {
114        self.lock().answering = Some((subscription_id.to_string(), generation));
115    }
116
117    pub fn end_answer(&self) {
118        self.lock().answering = None;
119    }
120
121    /// The subscription an action addresses: the one it names, else the REQ being answered.
122    /// Returns its id, its filters, and whether `limit` applies (only to the answer to the REQ
123    /// itself — NIP-01's limit is on the initial query).
124    pub fn target(&self, named: Option<&str>) -> Result<(String, Vec<Filter>, bool), String> {
125        let subs = self.lock();
126        let answering = subs.answering.clone();
127        let id = match (named.filter(|n| !n.is_empty()), &answering) {
128            (Some(n), _) => n.to_string(),
129            (None, Some((id, _))) => id.clone(),
130            (None, None) => {
131                return Err(
132                    "this action can only run while answering a nostr_req, or with the \
133                     subscription_id of a subscription this connection has open"
134                        .to_string(),
135                )
136            }
137        };
138        let Some(sub) = subs.open.get(&id) else {
139            return Err(format!(
140                "subscription {:?} is not open on this connection",
141                crate::utils::truncate_for_log(&id, 64)
142            ));
143        };
144        let apply_limit = answering
145            .as_ref()
146            .is_some_and(|(a, generation)| *a == id && *generation == sub.generation);
147        Ok((id, sub.filters.clone(), apply_limit))
148    }
149
150    /// The subscriptions whose filters match `event`, ignoring `limit`.
151    pub fn matching(&self, event: &Event) -> Vec<String> {
152        self.lock()
153            .open
154            .iter()
155            .filter(|(_, s)| s.filters.iter().any(|f| f.matches(event)))
156            .map(|(id, _)| id.clone())
157            .collect()
158    }
159}
160
161/// Every connection of one relay, for delivering accepted events to live subscriptions.
162#[derive(Debug, Default)]
163pub struct Relay {
164    connections: Mutex<HashMap<u32, Arc<ConnShared>>>,
165}
166
167impl Relay {
168    pub fn new() -> Self {
169        Self::default()
170    }
171
172    fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<u32, Arc<ConnShared>>> {
173        self.connections
174            .lock()
175            .unwrap_or_else(|poisoned| poisoned.into_inner())
176    }
177
178    pub fn add(&self, conn: Arc<ConnShared>) {
179        self.lock().insert(conn.connection_id, conn);
180    }
181
182    pub fn remove(&self, connection_id: u32) {
183        self.lock().remove(&connection_id);
184    }
185
186    /// Send an accepted event to every open subscription it matches, on every connection —
187    /// the publisher's own included, as NIP-01 relays do. Returns how many subscriptions got it.
188    pub fn broadcast(&self, event: &Event) -> usize {
189        let connections: Vec<Arc<ConnShared>> = self.lock().values().cloned().collect();
190        let mut delivered = 0;
191        for conn in connections {
192            for subscription_id in conn.matching(event) {
193                conn.send(wire::event_message(&subscription_id, event));
194                delivered += 1;
195            }
196        }
197        delivered
198    }
199}