Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-ui-web/src/feed.rs

//! The live feed, over server-sent events. An `EventSource` reconnects after a
//! network drop but NOT after an HTTP error status: when the session ends the
//! server answers 401 and the browser gives up, so a closed source sends the
//! operator to the login page.
//!
//! The lines come off monetize's `Events.Watch` stream (the console's server
//! holds one and fans it out), with a five-second poll as the server's
//! fallback. The console does not render them: every delta bumps `reread`,
//! and the page re-reads its outcomes. The step-by-step feed that used to sit
//! on the ledger page was the dump `docs/v2/07-console.md` rule 6 deletes.

use leptos::prelude::*;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Connection {
    Connecting,
    Live,
    Ended,
}

#[derive(Clone, Copy)]
pub struct Feed {
    pub connection: RwSignal<Connection>,
    /// Bumped on every delta, so a list re-reads instead of patching itself.
    /// The delta's content is not rendered anywhere (`docs/v2/07-console.md` rule 6: outcomes,
    /// not steps) — the re-read is the whole point of the feed.
    pub reread: RwSignal<u32>,
}

/// **The page's feed**: the one the estate shell opened ([`provide_feed`]),
/// else a stream of the caller's own.
///
/// ONE stream per page, not one per panel: a browser keeps six HTTP/1.1
/// connections per origin, and every `EventSource` holds one for as long as it
/// lives. An estate's Customers draws a panel set per product and Money four
/// panels that each re-read on the feed — one stream each was enough to use up
/// the six and leave the page's own reads queued behind them for good
/// (MEASURED 2026-10-02 in the picture walk: the Customers page of a
/// four-product estate never finished loading).
pub fn use_feed() -> Feed {
    use_context::<Feed>().unwrap_or_else(open_feed)
}

/// Open the one stream every panel under the caller shares.
pub fn provide_feed() -> Feed {
    let feed = open_feed();
    provide_context(feed);
    feed
}

/// Open a stream for as long as the calling component lives.
fn open_feed() -> Feed {
    let feed = Feed {
        connection: RwSignal::new(Connection::Connecting),
        reread: RwSignal::new(0),
    };
    // ONE feed: the ONE monetize's, whichever estate the shell shows.
    let Ok(source) = web_sys::EventSource::new("/api/events") else {
        feed.connection.set(Connection::Ended);
        return feed;
    };
    let on_open =
        Closure::<dyn FnMut(web_sys::Event)>::new(move |_| feed.connection.set(Connection::Live));
    let _ = source.add_event_listener_with_callback("open", on_open.as_ref().unchecked_ref());
    let on_message =
        Closure::<dyn FnMut(web_sys::MessageEvent)>::new(move |_: web_sys::MessageEvent| {
            feed.connection.set(Connection::Live);
            feed.reread.update(|n| *n = n.wrapping_add(1));
        });
    let _ = source.add_event_listener_with_callback("feed", on_message.as_ref().unchecked_ref());
    let on_error = Closure::<dyn FnMut(web_sys::Event)>::new({
        let source = source.clone();
        move |_| {
            if source.ready_state() == web_sys::EventSource::CLOSED {
                feed.connection.set(Connection::Ended);
                // A closed stream is not a lost session (a restart, a proxy): ask the server.
                // Only a console that says nobody is signed in leaves for the login page; a
                // signed-in operator stays on the page (the Sites order is never sent to enrol).
                leptos::task::spawn_local(async move {
                    if let Ok(s) = crate::api::get::<crate::app::Status>("/auth/status").await {
                        if crate::fmt::leaves_for_login(s.authenticated, s.open, s.passkey_enabled)
                        {
                            crate::app::go_to_login_because("session");
                        } else {
                            feed.reread.update(|n| *n = n.wrapping_add(1));
                        }
                    }
                });
            } else {
                feed.connection.set(Connection::Connecting);
            }
        }
    });
    let _ = source.add_event_listener_with_callback("error", on_error.as_ref().unchecked_ref());
    // Nothing here leaves the browser's single thread; the wrapper says so, and
    // owning the closures means a page opened repeatedly does not leak a set per visit.
    let owned = send_wrapper::SendWrapper::new((source, on_open, on_message, on_error));
    on_cleanup(move || {
        owned.0.close();
        drop(owned);
    });
    feed
}