mumu-event 0.1.1

event plugin for the mumu ecosystem
Documentation
// src/lib.rs
//
// MuMu "event" plugin: timers (timeout/interval) and per-tick loops.
// - Host (native): exposes a dynamic entrypoint `Cargo_lock` so it can be
//   loaded via extend("event").
// - WASM (web): exposes `register_all(&mut Interpreter)` so the wrapper can
//   statically bake the plugin in, just like math/array/string/regex.
//
// Provides dynamic functions:
//   • event:timeout(ms, cb)   — one-shot callback after ms
//   • event:interval(ms, cb)  — repeating callback every ms (returns handle)
//   • event:stop(handle)      — stops an interval/loop by handle
//   • event:loop(cb)          — run cb on every interpreter poll tick
//
// The plugin also installs a poller that drives timeouts/intervals/loops.

use core_mumu::{Interpreter, Value, FunctionValue};
use std::{
    ffi::{c_void, CStr},
    sync::{Arc, Mutex},
};
use std::time::Instant;

mod timeout;
mod interval;
mod r#loop; // `loop` is a Rust keyword

// Re-export bridges so users can call them directly if desired.
pub use timeout::event_timeout_bridge;
pub use interval::{event_interval_bridge, event_stop_bridge};
pub use r#loop::event_loop_bridge;

use lazy_static::lazy_static;

lazy_static! {
    // Timeout tasks are managed locally here; intervals/loops live in their modules.
    static ref TIMEOUT_TASKS: Mutex<Vec<timeout::TimeoutTask>> = Mutex::new(vec![]);
}

/// Main poller – executes timeouts, intervals, and endless loops every tick.
/// Returns how many tasks remain alive (timeouts pending + intervals + loops).
fn poller_fn(interp: &mut Interpreter) -> usize {
    let now = Instant::now();

    // ── 1) Timeouts ──────────────────────────────────────────────────────
    {
        let mut tasks = TIMEOUT_TASKS.lock().unwrap();
        let mut expired = Vec::new();
        for (i, t) in tasks.iter().enumerate() {
            if now >= t.deadline {
                let _ = timeout::execute_callback(interp, &t.callback);
                expired.push(i);
            }
        }
        for &i in expired.iter().rev() {
            tasks.remove(i);
        }
    }
    let remaining_timeouts = TIMEOUT_TASKS.lock().unwrap().len();

    // ── 2) Intervals ─────────────────────────────────────────────────────
    {
        let mut iv = interval::INTERVAL_TASKS.lock().unwrap();
        for i in (0..iv.len()).rev() {
            let task = &mut iv[i];
            if now >= task.next_tick {
                let _ = interval::execute_interval_callback(interp, &task.callback);
                if !task.canceled {
                    task.next_tick = now + task.period;
                } else {
                    iv.remove(i);
                }
            }
        }
    }
    let remaining_intervals = interval::INTERVAL_TASKS.lock().unwrap().len();

    // ── 3) Endless loops ─────────────────────────────────────────────────
    let remaining_loops = r#loop::execute_loop_callbacks(interp);

    remaining_timeouts + remaining_intervals + remaining_loops
}

/// Helper to be called from `timeout.rs` when a new timeout is created.
pub fn push_timeout_task(task: timeout::TimeoutTask, _interp: &mut Interpreter) {
    TIMEOUT_TASKS.lock().unwrap().push(task);
}

/// Register all event:* bridges and install the background poller.
/// This is used by host builds (via the dynamic entrypoint) **and**
/// by WASM builds where the wrapper calls this directly.
pub fn register_all(interp: &mut Interpreter) {
    // event:timeout
    {
        let f = Arc::new(Mutex::new(event_timeout_bridge));
        interp.register_dynamic_function("event:timeout", f.clone());
        interp.set_variable(
            "event:timeout",
            Value::Function(Box::new(FunctionValue::Named("event:timeout".into()))),
        );
    }

    // event:interval
    {
        let f = Arc::new(Mutex::new(event_interval_bridge));
        interp.register_dynamic_function("event:interval", f.clone());
        interp.set_variable(
            "event:interval",
            Value::Function(Box::new(FunctionValue::Named("event:interval".into()))),
        );
    }

    // event:stop
    {
        let f = Arc::new(Mutex::new(event_stop_bridge));
        interp.register_dynamic_function("event:stop", f.clone());
        interp.set_variable(
            "event:stop",
            Value::Function(Box::new(FunctionValue::Named("event:stop".into()))),
        );
    }

    // event:loop
    {
        let f = Arc::new(Mutex::new(event_loop_bridge));
        interp.register_dynamic_function("event:loop", f.clone());
        interp.set_variable(
            "event:loop",
            Value::Function(Box::new(FunctionValue::Named("event:loop".into()))),
        );
    }

    // Install the background poller (drives timeouts/intervals/loops).
    interp.add_poller(Arc::new(Mutex::new(|intrp: &mut Interpreter| {
        poller_fn(intrp)
    })));
}

/* ──────────────────────────────────────────────────────────────────────────
   Dynamic loader entrypoint (host/native only)
   The core loader looks up the symbol **Cargo_lock** exactly.
   We keep it out of wasm builds so the crate links cleanly to the web target.
   ──────────────────────────────────────────────────────────────────────── */
#[cfg(not(target_arch = "wasm32"))]
#[no_mangle]
pub unsafe extern "C" fn Cargo_lock(
    interp_ptr: *mut c_void,
    extra_str: *const c_void,
) -> i32 {
    if interp_ptr.is_null() {
        return 1;
    }
    let interp = &mut *(interp_ptr as *mut Interpreter);

    if interp.is_verbose() {
        eprintln!("[event] Cargo_lock ⇒ initialising plugin");
        if !extra_str.is_null() {
            let c = CStr::from_ptr(extra_str as *const i8);
            eprintln!("[event] extra arg = '{}'", c.to_string_lossy());
        }
    }

    register_all(interp);

    if interp.is_verbose() {
        eprintln!("[event] Cargo_lock ⇒ registration complete");
    }
    0
}