Skip to main content

iroh_http_core/
registry.rs

1//! Global endpoint registry shared by all FFI adapters.
2//!
3//! Centralises the `Slab<IrohEndpoint>` that was previously triplicated
4//! across Node, Deno, and Tauri adapters.  Handles are `u64`, consistent
5//! with stream handles from `slotmap`.
6
7use std::sync::{Mutex, OnceLock};
8
9use slab::Slab;
10
11use crate::endpoint::IrohEndpoint;
12
13fn endpoint_slab() -> &'static Mutex<Slab<IrohEndpoint>> {
14    static S: OnceLock<Mutex<Slab<IrohEndpoint>>> = OnceLock::new();
15    S.get_or_init(|| Mutex::new(Slab::new()))
16}
17
18/// Insert an endpoint into the global registry and return its handle.
19pub fn insert_endpoint(ep: IrohEndpoint) -> u64 {
20    endpoint_slab()
21        .lock()
22        .unwrap_or_else(|e| e.into_inner())
23        .insert(ep) as u64
24}
25
26/// Look up an endpoint by handle (cheap `Arc` clone).
27pub fn get_endpoint(handle: u64) -> Option<IrohEndpoint> {
28    endpoint_slab()
29        .lock()
30        .unwrap_or_else(|e| e.into_inner())
31        .get(handle as usize)
32        .cloned()
33}
34
35/// Remove an endpoint from the registry, returning it if it existed.
36pub fn remove_endpoint(handle: u64) -> Option<IrohEndpoint> {
37    let mut slab = endpoint_slab().lock().unwrap_or_else(|e| e.into_inner());
38    if slab.contains(handle as usize) {
39        Some(slab.remove(handle as usize))
40    } else {
41        None
42    }
43}
44
45/// Drain the entire registry and force-close every endpoint.
46///
47/// Called on `WindowEvent::Destroyed` in the Tauri plugin to prevent QUIC
48/// socket leaks when the webview hot-reloads without calling `close_endpoint`.
49///
50/// Removes all entries from the registry synchronously, then drives
51/// `close_force` on each in a background thread (safe to call from any
52/// context, including synchronous window-event handlers outside a tokio task).
53pub fn close_all_endpoints() {
54    let endpoints: Vec<IrohEndpoint> = {
55        let mut slab = endpoint_slab().lock().unwrap_or_else(|e| e.into_inner());
56        let keys: Vec<usize> = slab.iter().map(|(k, _)| k).collect();
57        keys.into_iter()
58            .filter_map(|k| {
59                if slab.contains(k) {
60                    Some(slab.remove(k))
61                } else {
62                    None
63                }
64            })
65            .collect()
66    };
67    if endpoints.is_empty() {
68        return;
69    }
70    // Spawn a background OS thread with its own single-threaded tokio runtime
71    // so that `close_force` (which is async) can be awaited without requiring
72    // the caller to be inside an existing tokio context.
73    std::thread::spawn(move || {
74        if let Ok(rt) = tokio::runtime::Builder::new_current_thread()
75            .enable_all()
76            .build()
77        {
78            rt.block_on(async move {
79                for ep in endpoints {
80                    ep.close_force().await;
81                }
82            });
83        }
84        // If runtime creation fails, `endpoints` is dropped here — the Arc
85        // refcount reaches zero, which still frees the registry entry.
86        // The OS reclaims the underlying QUIC sockets on process exit.
87    });
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn close_all_endpoints_is_idempotent_on_empty_registry() {
96        // Should not panic when there are no endpoints.
97        close_all_endpoints();
98    }
99}