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}