rings_node/extension/ext/registry.rs
1#![warn(missing_docs)]
2//! Router + capability core.
3//!
4//! [`Extensions`] registers `(Protocol, Interpret)` pairs by namespace. Each interpreter is
5//! handed a namespace-scoped [`Scope`] (overlay `send` / `did` / self-`inject`, confined to its
6//! own namespace) — *not* the router-internal `Core`. `Core` is the crate-private capability
7//! that also routes an inbound [`Envelope`] to its protocol and drives the bounded re-injection
8//! fixpoint; the registry stays uniform (everything erased to the internal `Handler`) while each
9//! extension's shell is its own. Extension authors see only `Protocol` / `Interpret` / `Scope` /
10//! `Transition` / `Extensions`.
11
12use std::collections::HashMap;
13use std::collections::VecDeque;
14use std::ops::Deref;
15use std::sync::Arc;
16use std::sync::Mutex;
17use std::sync::RwLock;
18
19use bytes::Bytes;
20use rings_core::dht::Did;
21
22use super::Ctx;
23use super::Envelope;
24use super::Inbound;
25use super::Interpret;
26use super::MaybeSend;
27use super::Protocol;
28use super::Reject;
29use super::Transition;
30use super::Wire;
31use crate::error::Error;
32use crate::error::Result;
33use crate::processor::Processor;
34
35/// Upper bound on re-injection iterations per inbound message, so a misbehaving
36/// protocol/effect cycle cannot diverge.
37const MAX_FIXPOINT_STEPS: u32 = 1024;
38
39/// Type-erased handler stored in the registry: native is `Send + Sync`, browser not.
40#[cfg(not(feature = "browser"))]
41pub(crate) type DynHandler = dyn Handler + Send + Sync;
42/// Type-erased handler stored in the registry.
43#[cfg(feature = "browser")]
44pub(crate) type DynHandler = dyn Handler;
45
46type HandlerMap = RwLock<HashMap<String, Arc<DynHandler>>>;
47
48/// Erased, runtime-facing handler — the router-internal ABI. Implemented once, generically, by
49/// `Runner`; protocol authors never name it (they write `Protocol` + `Interpret`).
50#[cfg_attr(feature = "browser", async_trait::async_trait(?Send))]
51#[cfg_attr(not(feature = "browser"), async_trait::async_trait)]
52pub(crate) trait Handler {
53 /// Decode → step (pure, committed) → run the protocol's effects, returning re-injected
54 /// messages. `handle : (from, payload) → IO [Inbound]`.
55 async fn handle(&self, core: &Core, from: Did, payload: Bytes) -> Result<Vec<Inbound>>;
56}
57
58/// The router-internal capability. Cloneable and `'static` so a long-running engine task can
59/// keep a copy and feed events back via [`inject`](Core::inject). Not handed to extension
60/// shells — they get a namespace-scoped [`Scope`] instead.
61#[derive(Clone)]
62pub(crate) struct Core {
63 processor: Arc<Processor>,
64 handlers: Arc<HandlerMap>,
65}
66
67impl Core {
68 /// This node's DID.
69 pub fn did(&self) -> Did {
70 self.processor.did()
71 }
72
73 /// Put a message on the overlay to `to` under `namespace`.
74 pub async fn send(&self, to: Did, namespace: &str, payload: Bytes) -> Result<()> {
75 let envelope = Envelope::new(namespace, payload);
76 self.processor.send_envelope(to, &envelope).await?;
77 Ok(())
78 }
79
80 /// Re-enter the router with a *self*-addressed message (`from = this node`): a locally
81 /// injected command, or an engine task feeding a lifecycle event back to its protocol.
82 pub async fn inject(&self, namespace: &str, payload: Bytes) -> Result<()> {
83 self.dispatch(self.did(), Envelope::new(namespace, payload))
84 .await
85 }
86
87 /// Route an inbound [`Envelope`] to its protocol and drive the bounded re-injection
88 /// fixpoint. Unknown namespaces are logged and dropped (non-fatal).
89 ///
90 /// This is the **authenticated ingress** capability: the caller chooses `from`, so a
91 /// protocol's `decode` will attribute the resulting event to that DID (for the relay, a
92 /// `from != me` envelope becomes a peer `Frame`). It is therefore `pub(crate)` — only the
93 /// router path may call it, and only [`Backend`](crate::extension::Backend) does, with
94 /// `from` taken from the message's verified signer. Extension code reaches the router only
95 /// through [`inject`](Core::inject) (self-addressed, `from = self.did()`); it can never
96 /// forge a remote `from`.
97 pub(crate) async fn dispatch(&self, from: Did, envelope: Envelope) -> Result<()> {
98 let mut queue: VecDeque<Inbound> = VecDeque::new();
99 queue.push_back(Inbound {
100 namespace: envelope.namespace,
101 from,
102 payload: envelope.payload,
103 });
104
105 let mut budget = MAX_FIXPOINT_STEPS;
106 while let Some(Inbound {
107 namespace,
108 from,
109 payload,
110 }) = queue.pop_front()
111 {
112 if budget == 0 {
113 return Err(Error::ExtensionError(format!(
114 "fixpoint budget ({MAX_FIXPOINT_STEPS}) exhausted; last namespace {namespace:?}"
115 )));
116 }
117 budget -= 1;
118
119 match self.handler(namespace.as_str()) {
120 Some(handler) => queue.extend(handler.handle(self, from, payload).await?),
121 None => tracing::debug!(
122 "no protocol registered for namespace {:?}, dropping",
123 namespace
124 ),
125 }
126 }
127 Ok(())
128 }
129
130 fn handler(&self, namespace: &str) -> Option<Arc<DynHandler>> {
131 self.handlers.read().ok()?.get(namespace).map(Arc::clone)
132 }
133}
134
135/// A **namespace-scoped** capability handed to an [`Interpret`] shell — the effectful
136/// counterpart of the pure side's read-only [`Ctx`]. Every action is confined to the
137/// interpreter's own namespace: it may `send` to peers and self-`inject` only there, and it
138/// can neither reach another namespace nor forge a remote `from`. This is what keeps the
139/// capability honest: an extension shell cannot use the router as a generic
140/// inject-any-namespace bus (e.g. manufacture another extension's lifecycle events). Cloneable
141/// and `'static`, so a long-running engine task can keep a copy.
142#[derive(Clone)]
143pub struct Scope {
144 core: Core,
145 namespace: String,
146}
147
148impl Scope {
149 /// Confine `core` to `namespace`.
150 pub(crate) fn new(core: Core, namespace: String) -> Self {
151 Self { core, namespace }
152 }
153
154 /// This node's DID.
155 pub fn did(&self) -> Did {
156 self.core.did()
157 }
158
159 /// The namespace this scope is confined to.
160 pub fn namespace(&self) -> &str {
161 self.namespace.as_str()
162 }
163
164 /// Put a message on the overlay to `to`, under this interpreter's own namespace.
165 pub async fn send(&self, to: Did, payload: Bytes) -> Result<()> {
166 self.core.send(to, self.namespace.as_str(), payload).await
167 }
168
169 /// Self-inject `payload` into this interpreter's **own** namespace (`from = this node`).
170 ///
171 /// `pub(crate)`: this is the **long-lived lifecycle sink** for an extension's own engine
172 /// (e.g. the relay's spawned socket tasks reporting `Accepted`/`Untrack` later), and it
173 /// starts a **fresh** [`dispatch`](Core::dispatch) fixpoint with its own
174 /// `MAX_FIXPOINT_STEPS` budget — it is *not* part of the bounded re-injection fixpoint that
175 /// drives a single inbound. The synchronous per-effect feedback path is the `Vec<Bytes>`
176 /// returned from [`Interpret::run`], which the router re-injects within the current budget.
177 /// A third-party shell therefore gets only that bounded return path, never this re-entrant
178 /// sink, so it cannot recurse `inject` to escape the budget.
179 pub(crate) async fn inject(&self, payload: Bytes) -> Result<()> {
180 self.core.inject(self.namespace.as_str(), payload).await
181 }
182}
183
184/// Adapter binding a pure [`Protocol`] to its [`Interpret`] shell and owned state; erased to
185/// [`Handler`]. Protocol authors never write this.
186struct Runner<P: Protocol, I> {
187 protocol: P,
188 interpret: I,
189 state: Mutex<P::State>,
190}
191
192#[cfg_attr(feature = "browser", async_trait::async_trait(?Send))]
193#[cfg_attr(not(feature = "browser"), async_trait::async_trait)]
194impl<P, I> Handler for Runner<P, I>
195where
196 P: Protocol + MaybeSend + 'static,
197 P::State: MaybeSend + 'static,
198 P::Effect: MaybeSend,
199 I: Interpret<Effect = P::Effect> + MaybeSend + 'static,
200{
201 async fn handle(&self, core: &Core, from: Did, payload: Bytes) -> Result<Vec<Inbound>> {
202 // Boundary: decode raw bytes to a typed event. An undecodable/foreign message is an
203 // explicit drop here, not a silent `Transition::pure` deep in `step`.
204 let event = match self.protocol.decode(Wire {
205 from,
206 me: core.did(),
207 payload: payload.as_ref(),
208 }) {
209 Ok(event) => event,
210 Err(Reject(why)) => {
211 tracing::debug!("drop on {}: {why}", self.protocol.namespace());
212 return Ok(Vec::new());
213 }
214 };
215
216 // Pure region: a brief *synchronous* critical section — read state, run `step`,
217 // commit next state. No `.await` inside, so the std `Mutex` is correct and the state
218 // fold stays serial per protocol (state-machine semantics, not a limitation;
219 // different protocols and all effects below run concurrently). The commit is the
220 // logical transition point; effect failures that matter come back as events.
221 let effects = {
222 let mut guard = self.state.lock().map_err(|_| Error::Lock)?;
223 let Transition { state, effects } = self.protocol.step(
224 Ctx {
225 did: core.did(),
226 state: guard.deref(),
227 },
228 event,
229 );
230 *guard = state;
231 effects
232 };
233
234 // Impure region (lock released): run the protocol's own effects via its interpreter,
235 // handing it only a namespace-scoped capability. Each payload it returns is re-injected
236 // into *this* namespace with `from = this node` — the router fixes the provenance, so a
237 // shell cannot forge a target namespace or a remote `from`.
238 let namespace = self.protocol.namespace().to_string();
239 let scope = Scope::new(core.clone(), namespace.clone());
240 let mut reinjected = Vec::new();
241 for effect in effects {
242 for payload in self.interpret.run(&scope, effect).await? {
243 reinjected.push(Inbound {
244 namespace: namespace.clone(),
245 from: core.did(),
246 payload,
247 });
248 }
249 }
250 Ok(reinjected)
251 }
252}
253
254/// Registry of `(Protocol, Interpret)` pairs by namespace, plus the router-internal `Core`.
255/// Cheaply cloneable and shared (interior mutability) so the
256/// [`Provider`](crate::provider::Provider) and the inbound callback see the same table.
257#[derive(Clone)]
258pub struct Extensions {
259 core: Core,
260}
261
262impl Extensions {
263 /// Empty registry over a processor (the source of overlay `send` / `did`).
264 pub fn new(processor: Arc<Processor>) -> Self {
265 Self {
266 core: Core {
267 processor,
268 handlers: Arc::new(RwLock::new(HashMap::new())),
269 },
270 }
271 }
272
273 /// The capability handle (overlay `send` / `did` / self-addressed `inject`). `pub(crate)`:
274 /// public holders of an `Extensions` get **registration only** (`register` / `replace` /
275 /// `contains` / `register_many`), never a raw [`Core`]. An extension's local injection is
276 /// exposed through its own typed handle (e.g. `RelayHandle`), so application code cannot use
277 /// a generic inject-any-namespace bus to forge engine-lifecycle commands like the relay's
278 /// `Accepted` / `Untrack`.
279 pub(crate) fn core(&self) -> Core {
280 self.core.clone()
281 }
282
283 /// Register a protocol together with its interpreter under the protocol's namespace.
284 /// Errors if the namespace is already taken — use [`replace`](Extensions::replace) for
285 /// intentional replacement (no more silent overwrite).
286 pub fn register<P, I>(&self, protocol: P, interpret: I) -> Result<()>
287 where
288 P: Protocol + MaybeSend + 'static,
289 P::State: MaybeSend + 'static,
290 P::Effect: MaybeSend,
291 I: Interpret<Effect = P::Effect> + MaybeSend + 'static,
292 {
293 self.insert(protocol, interpret, false)
294 }
295
296 /// Like [`register`](Extensions::register) but replaces an existing protocol on the same
297 /// namespace instead of erroring. For deliberate hot-swaps.
298 pub fn replace<P, I>(&self, protocol: P, interpret: I) -> Result<()>
299 where
300 P: Protocol + MaybeSend + 'static,
301 P::State: MaybeSend + 'static,
302 P::Effect: MaybeSend,
303 I: Interpret<Effect = P::Effect> + MaybeSend + 'static,
304 {
305 self.insert(protocol, interpret, true)
306 }
307
308 /// Register several protocols **atomically**: build every runner, then under a single write
309 /// lock verify that none of their namespaces is taken (by an existing registration or by a
310 /// duplicate within the batch) and insert them all — or change nothing and return `Err`. The
311 /// pairs share the type `P`/`I` (e.g. the relay's TCP + UDP `Relay<T>` instances), so a
312 /// partial install can never leave one namespace claimed while the caller gets no handle.
313 pub fn register_many<P, I>(&self, items: Vec<(P, I)>) -> Result<()>
314 where
315 P: Protocol + MaybeSend + 'static,
316 P::State: MaybeSend + 'static,
317 P::Effect: MaybeSend,
318 I: Interpret<Effect = P::Effect> + MaybeSend + 'static,
319 {
320 // Build (namespace, runner) outside the lock.
321 let prepared: Vec<(String, Arc<DynHandler>)> = items
322 .into_iter()
323 .map(|(protocol, interpret)| {
324 let namespace = protocol.namespace().to_string();
325 let state = Mutex::new(protocol.init());
326 let runner: Arc<DynHandler> = Arc::new(Runner {
327 protocol,
328 interpret,
329 state,
330 });
331 (namespace, runner)
332 })
333 .collect();
334
335 let mut handlers = self.core.handlers.write().map_err(|_| Error::Lock)?;
336 // Check-all (existing table + intra-batch duplicates) before mutating anything.
337 for (index, (namespace, _)) in prepared.iter().enumerate() {
338 let duplicate_in_batch = prepared[..index].iter().any(|(seen, _)| seen == namespace);
339 if duplicate_in_batch || handlers.contains_key(namespace) {
340 return Err(Error::ExtensionError(format!(
341 "namespace {namespace:?} is already registered"
342 )));
343 }
344 }
345 // All free: insert the whole batch.
346 for (namespace, runner) in prepared {
347 handlers.insert(namespace, runner);
348 }
349 Ok(())
350 }
351
352 fn insert<P, I>(&self, protocol: P, interpret: I, replace: bool) -> Result<()>
353 where
354 P: Protocol + MaybeSend + 'static,
355 P::State: MaybeSend + 'static,
356 P::Effect: MaybeSend,
357 I: Interpret<Effect = P::Effect> + MaybeSend + 'static,
358 {
359 let namespace = protocol.namespace().to_string();
360 let state = Mutex::new(protocol.init());
361 let runner: Arc<DynHandler> = Arc::new(Runner {
362 protocol,
363 interpret,
364 state,
365 });
366 let mut handlers = self.core.handlers.write().map_err(|_| Error::Lock)?;
367 if !replace && handlers.contains_key(&namespace) {
368 return Err(Error::ExtensionError(format!(
369 "namespace {namespace:?} is already registered"
370 )));
371 }
372 handlers.insert(namespace, runner);
373 Ok(())
374 }
375
376 /// Whether a namespace is registered.
377 pub fn contains(&self, namespace: &str) -> bool {
378 self.core
379 .handlers
380 .read()
381 .map(|h| h.contains_key(namespace))
382 .unwrap_or(false)
383 }
384
385 /// Route a decoded envelope (inbound entry point). `pub(crate)`: the authenticated ingress
386 /// belongs to the router path ([`Backend`](crate::extension::Backend)), not to public
387 /// holders of an `Extensions` value (which get registration + a self-addressed
388 /// [`Core`](Core::inject), never the ability to forge a remote `from`). See
389 /// [`Core::dispatch`].
390 pub(crate) async fn dispatch(&self, from: Did, envelope: Envelope) -> Result<()> {
391 self.core.dispatch(from, envelope).await
392 }
393}