LCOV - code coverage report
Current view: top level - antares-wasm/src - lib.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 0.0 % 57 0
Test Date: 2026-09-21 10:31:06 Functions: 0.0 % 8 0

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! The broker in a browser tab.
       3              : //!
       4              : //! Everything above the socket is unchanged — the same axum router, the same
       5              : //! handlers, the same memory store through the store seam. The ONE thing that
       6              : //! cannot cross is the TCP listener (browsers have no inbound sockets), so a
       7              : //! Service Worker feeds requests in instead and `handle` drives the
       8              : //! router directly with `tower::Service::call`, exactly as the native
       9              : //! `main.rs` accept loop does per connection.
      10              : //!
      11              : //! What the browser build is NOT: no NATS, no MQTT, no Postgres, no
      12              : //! roles. `bus=local` and the memory store are the only shapes that exist
      13              : //! here, which is why this crate turns `antares-api`'s default features off.
      14              : #![cfg_attr(not(test), warn(clippy::expect_used))]
      15              : 
      16              : use axum::body::Body;
      17              : use http_body_util::BodyExt;
      18              : use tower::Service;
      19              : 
      20              : /// One broker instance: the composed router plus the state it owns.
      21              : pub struct Broker {
      22              :     router: axum::Router,
      23              : }
      24              : 
      25              : impl Default for Broker {
      26            0 :     fn default() -> Self {
      27            0 :         Self::new()
      28            0 :     }
      29              : }
      30              : 
      31              : impl Broker {
      32              :     /// Build the router over an in-memory store. Mirrors the native wiring
      33              :     /// minus the pieces that need a socket or a pool.
      34            0 :     pub fn new() -> Self {
      35            0 :         Self::with_store(antares_sql::store::Store::default(), "memory")
      36            0 :     }
      37              : 
      38              :     /// The same wiring over an externally-constructed store — the OPFS-backed
      39              :     /// store enters here; `mode` is what `/q/health` reports.
      40            0 :     pub fn with_store(store: antares_sql::store::Store, mode: &str) -> Self {
      41            0 :         Self::with_store_alias(store, mode, None)
      42            0 :     }
      43              : 
      44              :     /// `host_alias` names this instance in Via chains — must be distinct per
      45              :     /// instance in a federation, or loop detection 508s every forward.
      46            0 :     pub fn with_store_alias(
      47            0 :         store: antares_sql::store::Store,
      48            0 :         mode: &str,
      49            0 :         host_alias: Option<String>,
      50            0 :     ) -> Self {
      51            0 :         let store = std::sync::Arc::new(antares_sql::store::any::AnyStore::Mem(store));
      52              :         // wasm compositions only ever carry the Mem arm — anything but a
      53              :         // known Mem-arm mode name is a caller bug, defaulted to memory.
      54            0 :         let mut state = antares_api::AppState::with_drivers(
      55            0 :             host_alias.unwrap_or_else(|| "antares-wasm".to_owned()),
      56            0 :             store.clone(),
      57            0 :             store,
      58            0 :             mode,
      59              :         );
      60              :         // Same in-process matcher/notifier path as bus=local: the
      61              :         // store's change hook feeds it, no bus process exists to talk to.
      62              :         // The browser constructors are JS-callable and cannot await, and
      63              :         // every driver in a wasm composition is the in-process memory arm,
      64              :         // whose futures complete on their first poll. One that suspended
      65              :         // would mean this composition grew a step no browser build has: the
      66              :         // matcher then takes the store-scan fallback `wire` documents
      67              :         // instead of the page losing its broker.
      68            0 :         if futures_util::FutureExt::now_or_never(antares_api::wire(&mut state)).is_none() {
      69            0 :             tracing::error!(
      70              :                 "wiring did not complete synchronously; \
      71              :                  matching falls back to a store scan per change"
      72              :             );
      73            0 :         }
      74              :         // 5.8.1.4: distributed subscriptions hand this URL to the remote
      75              :         // broker as the notification callback. wasm32 has no process env
      76              :         // (the native default reads ANTARES_PUBLIC_URL + appends the port),
      77              :         // so the SAME variable comes off `globalThis` — set it before
      78              :         // construction, like ANTARES_SWEEP_SECS below. Absent → the portless
      79              :         // host-alias default, which no peer outside a browser can dial.
      80              :         #[cfg(target_arch = "wasm32")]
      81              :         if let Some(url) = js_sys::Reflect::get(&js_sys::global(), &"ANTARES_PUBLIC_URL".into())
      82              :             .ok()
      83              :             .and_then(|v| v.as_string())
      84              :             .filter(|s| !s.is_empty())
      85              :         {
      86              :             state.public_url = url;
      87              :         }
      88              :         // 4.22 GC: the native broker sweeps on a tokio interval (main.rs,
      89              :         // ANTARES_SWEEP_SECS). The browser has no env, so the SAME variable is
      90              :         // read off `globalThis.ANTARES_SWEEP_SECS` (seconds, set before
      91              :         // construction — the playground forwards a ?ANTARES_SWEEP_SECS= URL
      92              :         // param into both the worker and in-page contexts); absent → 60 s.
      93              :         // Without this loop the OPFS file grows without bound under ticking
      94              :         // transient attributes — reads filter expired instances but nothing
      95              :         // would ever delete them.
      96              :         #[cfg(target_arch = "wasm32")]
      97              :         {
      98              :             let store = state.store.clone();
      99              :             let sweep_ms = js_sys::Reflect::get(&js_sys::global(), &"ANTARES_SWEEP_SECS".into())
     100              :                 .ok()
     101              :                 .and_then(|v| v.as_f64())
     102              :                 .filter(|s| *s > 0.0)
     103              :                 .map(|s| (s * 1000.0) as u32)
     104              :                 .unwrap_or(60_000);
     105              :             wasm_bindgen_futures::spawn_local(async move {
     106              :                 loop {
     107              :                     gloo_timers::future::TimeoutFuture::new(sweep_ms).await;
     108              :                     store.sweep_expired();
     109              :                 }
     110              :             });
     111              :         }
     112            0 :         Self {
     113            0 :             router: antares_api::router(state),
     114            0 :         }
     115            0 :     }
     116              : 
     117              :     /// Serve ONE request. The signature is the seam every front end reduces
     118              :     /// to: the Service Worker, the in-page API, and the Node shim
     119              :     /// all funnel here.
     120              :     ///
     121              :     /// `&self` on purpose: a federation forward to the loopback host re-enters
     122              :     /// this same instance WHILE an outer `handle` is suspended — `&mut`
     123              :     /// would make that a wasm-bindgen recursive-borrow error. Router clone is
     124              :     /// a cheap Arc bump and shares all state.
     125            0 :     pub async fn handle(&self, req: http::Request<Vec<u8>>) -> http::Response<Vec<u8>> {
     126            0 :         let (mut parts, body) = req.into_parts();
     127              :         // The native binary routes under NormalizePathLayer::trim_trailing_slash
     128              :         // (6.3 URLs arrive both with and without a trailing '/'); this seam is
     129              :         // the wasm equivalent — same trim, applied before the router sees it.
     130            0 :         if let Some(pq) = parts.uri.path_and_query() {
     131            0 :             let path = pq.path();
     132            0 :             let trimmed = path.trim_end_matches('/');
     133            0 :             if trimmed.len() != path.len() && !trimmed.is_empty() {
     134            0 :                 let new = match pq.query() {
     135            0 :                     Some(q) => format!("{trimmed}?{q}"),
     136            0 :                     None => trimmed.to_owned(),
     137              :                 };
     138            0 :                 if let Ok(uri) = new.parse() {
     139            0 :                     parts.uri = uri;
     140            0 :                 }
     141            0 :             }
     142            0 :         }
     143            0 :         let req = http::Request::from_parts(parts, Body::from(body));
     144            0 :         let mut router = self.router.clone();
     145            0 :         let resp = match router.call(req).await {
     146            0 :             Ok(r) => r,
     147              :             // The router is Infallible; keep the arm honest rather than
     148              :             // unwrapping (workspace lints deny unwrap outside tests).
     149              :             Err(_) => {
     150            0 :                 return http::Response::builder()
     151            0 :                     .status(500)
     152            0 :                     .body(Vec::new())
     153            0 :                     .unwrap_or_default()
     154              :             }
     155              :         };
     156            0 :         let (parts, body) = resp.into_parts();
     157            0 :         let bytes = body
     158            0 :             .collect()
     159            0 :             .await
     160            0 :             .map(|c| c.to_bytes().to_vec())
     161            0 :             .unwrap_or_default();
     162            0 :         http::Response::from_parts(parts, bytes)
     163            0 :     }
     164              : }
     165              : 
     166              : #[cfg(target_arch = "wasm32")]
     167              : mod browser;
     168              : #[cfg(target_arch = "wasm32")]
     169              : mod opfs;
     170              : #[cfg(target_arch = "wasm32")]
     171              : pub use browser::*;
        

Generated by: LCOV version 2.0-1