AkurAI Build
Menu

Public repository / master

AkurAI-Framework / crates/ui/src/callbacks.rs

Latest change 2ebaebff3945c75127afb6a9524ac1192993158b - akurai-ui: EventSource bridge (http::event_source, stream callbacks, host.js + native fake, testing helpers) by Ólafur Búi Ólafsson

//! The callback registry: Rust closures addressed by `u32` ids so the host can
//! call back into them (DOM events, fetch completions, timers).

use crate::dom::Event;
use crate::host::EventRef;
use crate::reactive::batch;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;

type Completion = Box<dyn FnOnce(u16, String)>;

enum Callback {
    Event(Box<dyn FnMut(Event)>),
    Once(Completion),
    /// A stream (e.g. an `EventSource`): called for every message until removed.
    Stream(Box<dyn FnMut(u16, String)>),
}

thread_local! {
    // `None` marks a callback that is currently executing (taken out).
    static REGISTRY: RefCell<HashMap<u32, Option<Callback>>> = RefCell::new(HashMap::new());
    static NEXT: Cell<u32> = const { Cell::new(1) };
}

fn insert(cb: Callback) -> u32 {
    let id = NEXT.with(|n| {
        let id = n.get();
        n.set(id + 1);
        id
    });
    REGISTRY.with(|r| r.borrow_mut().insert(id, Some(cb)));
    id
}

/// Register a repeating event handler.
pub(crate) fn on_event(f: impl FnMut(Event) + 'static) -> u32 {
    insert(Callback::Event(Box::new(f)))
}

/// Register a one-shot completion (`status`, `body`).
pub(crate) fn once(f: impl FnOnce(u16, String) + 'static) -> u32 {
    insert(Callback::Once(Box::new(f)))
}

/// Register a repeating `(status, body)` stream handler.
pub(crate) fn stream(f: impl FnMut(u16, String) + 'static) -> u32 {
    insert(Callback::Stream(Box::new(f)))
}

/// Forget a callback. Safe to call while it is executing.
pub(crate) fn remove(id: u32) {
    REGISTRY.with(|r| r.borrow_mut().remove(&id));
}

/// Number of live callbacks (leak checks in tests).
pub fn live() -> usize {
    REGISTRY.with(|r| r.borrow().len())
}

/// Run an event handler. Writes inside it are batched into one flush.
pub(crate) fn dispatch(id: u32, ev: EventRef) {
    let taken = REGISTRY.with(|r| r.borrow_mut().get_mut(&id).and_then(Option::take));
    let Some(Callback::Event(mut f)) = taken else {
        return;
    };
    batch(|| f(Event(ev)));
    REGISTRY.with(|r| {
        if let Some(slot) = r.borrow_mut().get_mut(&id) {
            *slot = Some(Callback::Event(f));
        }
    });
}

/// Run a completion: a one-shot is dropped afterwards, a stream is kept.
pub(crate) fn complete(id: u32, status: u16, body: String) {
    let is_stream = REGISTRY.with(|r| {
        matches!(
            r.borrow().get(&id),
            Some(Some(Callback::Stream(_))) | Some(None)
        )
    });
    if is_stream {
        let taken = REGISTRY.with(|r| r.borrow_mut().get_mut(&id).and_then(Option::take));
        let Some(Callback::Stream(mut f)) = taken else {
            return;
        };
        batch(|| f(status, body));
        REGISTRY.with(|r| {
            if let Some(slot) = r.borrow_mut().get_mut(&id) {
                *slot = Some(Callback::Stream(f));
            }
        });
        return;
    }
    let taken = REGISTRY.with(|r| r.borrow_mut().remove(&id).flatten());
    if let Some(Callback::Once(f)) = taken {
        batch(|| f(status, body));
    }
}