vertigo-cli 0.11.2

Reactive Real-DOM library with SSR for Rust - packaging/serving tool
Documentation
use parking_lot::RwLock;
use std::{
    collections::HashMap,
    sync::{Arc, OnceLock},
    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use tokio::sync::mpsc::{error::TryRecvError, unbounded_channel};
use vertigo::{
    JsJson, JsJsonSerialize,
    dev::command::{CommandForBrowser, ConsoleLogLevel, browser_response},
};
use wasmtime::{Engine, Module};

use crate::{
    commons::{ErrorCode, spawn::SpawnOwner},
    serve::html::FetchCache,
};

use super::{
    html::HtmlResponse,
    mount_path::MountConfig,
    request_state::RequestState,
    response_state::ResponseState,
    wasm::{Message, WasmInstance},
};

pub fn get_now() -> Duration {
    let start = SystemTime::now();
    match start.duration_since(UNIX_EPOCH) {
        Ok(duration) => duration,
        Err(err) => {
            log::error!("Time went backwards: {err}");
            Duration::from_secs(0)
        }
    }
}

pub type ServerStateMap = HashMap<String, Arc<ServerState>>;

static STATE: OnceLock<Arc<RwLock<ServerStateMap>>> = OnceLock::new();

#[derive(Clone)]
pub struct ServerState {
    engine: Engine,
    module: Module,
    pub mount_config: MountConfig,
    pub port_watch: Option<u16>,
}

impl ServerState {
    pub fn init(mount_config: &MountConfig) -> Result<(), ErrorCode> {
        Self::init_with_watch(mount_config, None)
    }

    pub fn init_with_watch(
        mount_config: &MountConfig,
        port_watch: Option<u16>,
    ) -> Result<(), ErrorCode> {
        let engine = Engine::default();

        let module = build_module_wasm(&engine, mount_config)?;

        let mutex = STATE.get_or_init(|| Arc::new(RwLock::new(ServerStateMap::new())));

        let mut guard = mutex.write();
        guard.insert(
            mount_config.mount_point().to_string(),
            Arc::new(Self {
                engine,
                module,
                mount_config: mount_config.clone(),
                port_watch,
            }),
        );

        Ok(())
    }

    pub fn global(mount_point: &str) -> Arc<ServerState> {
        let mutex = STATE.get_or_init(|| Arc::new(RwLock::new(ServerStateMap::new())));

        let guard = mutex.read();

        if let Some(state) = guard.get(mount_point) {
            return state.clone();
        }

        unreachable!();
    }

    pub async fn request(&self, url: &str) -> ResponseState {
        let (sender, mut receiver) = unbounded_channel::<Message>();

        let request = RequestState {
            url: url.to_string(),
            env: self.mount_config.env.clone(),
        };

        let fetch = FetchCache::new();

        let mut inst = WasmInstance::new(
            sender.clone(),
            &self.engine,
            &self.module,
            request,
            Arc::new({
                let sender = sender.clone();

                move |request: RequestState, command| match command {
                    CommandForBrowser::FetchCacheGet => {
                        browser_response::FetchCacheGet { data: None }.to_json()
                    }
                    CommandForBrowser::FetchExec { request, callback } => {
                        sender
                            .send(Message::FetchRequest { callback, request })
                            .inspect_err(|err| log::error!("Error sending FetchRequest: {err}"))
                            .unwrap_or_default();

                        JsJson::Null
                    }
                    CommandForBrowser::SetStatus { status } => {
                        sender
                            .send(Message::SetStatus(status))
                            .inspect_err(|err| log::error!("Error sending FetchRequest: {err}"))
                            .unwrap_or_default();

                        JsJson::Null
                    }
                    CommandForBrowser::IsBrowser => {
                        let response = browser_response::IsBrowser { value: false };

                        response.to_json()
                    }
                    CommandForBrowser::GetDateNow => {
                        let time = get_now().as_millis();

                        let response = browser_response::GetDateNow { value: time as u64 };

                        response.to_json()
                    }
                    CommandForBrowser::WebsocketRegister {
                        host: _,
                        callback: _,
                    } => JsJson::Null,
                    CommandForBrowser::WebsocketUnregister { callback: _ } => JsJson::Null,
                    CommandForBrowser::WebsocketSendMessage {
                        callback: _,
                        message: _,
                    } => JsJson::Null,
                    CommandForBrowser::TimerSet {
                        callback,
                        duration,
                        kind: _,
                    } => {
                        if duration == 0 {
                            sender
                                .send(Message::SetTimeoutZero { callback })
                                .inspect_err(|err| {
                                    log::error!("Error sending SetTimeoutZero: {err}")
                                })
                                .unwrap_or_default();
                        }

                        JsJson::Null
                    }
                    CommandForBrowser::TimerClear { callback: _ } => JsJson::Null,
                    CommandForBrowser::LocationCallback {
                        target: _,
                        mode: _,
                        callback: _,
                    } => JsJson::Null,
                    CommandForBrowser::LocationSet {
                        target: _,
                        mode: _,
                        value: _,
                    } => JsJson::Null,
                    CommandForBrowser::LocationGet { target: _ } => {
                        let url = request.url.clone();
                        browser_response::LocationGet { value: url }.to_json()
                    }
                    CommandForBrowser::CookieGet { name: _ } => {
                        browser_response::CookieGet { value: "".into() }.to_json()
                    }
                    CommandForBrowser::CookieSet {
                        name: _,
                        value: _,
                        expires_in: _,
                    } => JsJson::Null,
                    CommandForBrowser::CookieJsonGet { name: _ } => {
                        browser_response::CookieJsonGet {
                            value: JsJson::Null,
                        }
                        .to_json()
                    }
                    CommandForBrowser::CookieJsonSet {
                        name: _,
                        value: _,
                        expires_in: _,
                    } => JsJson::Null,
                    CommandForBrowser::GetEnv { name } => {
                        let env_value = request.env(name);

                        browser_response::GetEnv { value: env_value }.to_json()
                    }
                    CommandForBrowser::Log {
                        kind,
                        message,
                        arg2: _,
                        arg3: _,
                        arg4: _,
                    } => {
                        if kind == ConsoleLogLevel::Error {
                            log::warn!("{message}");
                        } else {
                            log::info!("{message}");
                        }

                        JsJson::Null
                    }
                    CommandForBrowser::TimezoneOffset => {
                        browser_response::TimezoneOffset { value: 0 }.to_json()
                    }
                    CommandForBrowser::HistoryBack => JsJson::Null,
                    CommandForBrowser::GetRandom { min, max: _ } => {
                        browser_response::GetRandom { value: min }.to_json()
                    }
                    CommandForBrowser::JsApiCall { commands: _ } => JsJson::Null,
                    CommandForBrowser::DomBulkUpdate { list } => {
                        sender
                            .send(Message::DomUpdate(list))
                            .inspect_err(|err| log::error!("Error sending DomUpdate: {err}"))
                            .unwrap_or_default();

                        JsJson::Null
                    }
                }
            }),
        );

        // -- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //TODO - ultimately, do not call call_vertigo_entry_function if something is returned by handle_url
        // -- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

        inst.call_vertigo_entry_function();

        if let Some(result) = inst.handle_url(url) {
            return result;
        }

        let spawn_resource = SpawnOwner::new({
            let sender = sender.clone();

            async move {
                tokio::time::sleep(std::time::Duration::from_secs(10)).await;
                let _ = sender.send(Message::TimeoutAndSendResponse);
            }
        });

        let mut html_response = HtmlResponse::new(
            sender.clone(),
            &self.mount_config,
            inst,
            self.mount_config.env.clone(),
            fetch,
        );

        loop {
            let message = receiver.try_recv();

            match message {
                Ok(message) => {
                    if let Some(response) = html_response.process_message(message) {
                        return response;
                    };
                    continue;
                }
                Err(TryRecvError::Empty) => {} // continue this iteration
                Err(TryRecvError::Disconnected) => {
                    break; // send response to browser
                }
            }

            if html_response.awaiting_response() {
                let message = receiver.recv().await;
                if let Some(message) = message
                    && let Some(response) = html_response.process_message(message)
                {
                    return response;
                };
            } else {
                break; // send response to browser
            }
        }

        spawn_resource.off();
        html_response.build_response()
    }
}

fn build_module_wasm(engine: &Engine, mount_path: &MountConfig) -> Result<Module, ErrorCode> {
    let full_wasm_path = mount_path.get_wasm_fs_path();

    log::info!("Mounting {} -> {full_wasm_path}", mount_path.mount_point());

    let wasm_content = match std::fs::read(&full_wasm_path) {
        Ok(wasm_content) => wasm_content,
        Err(error) => {
            log::error!("Problem reading the path: wasm_path={full_wasm_path}, error={error}");
            return Err(ErrorCode::ServeWasmReadFailed);
        }
    };

    let now = Instant::now();

    let module = match Module::from_binary(engine, &wasm_content) {
        Ok(module) => module,
        Err(err) => {
            log::error!("Wasm compilation error: error={err}");
            return Err(ErrorCode::ServeWasmCompileFailed);
        }
    };

    log::info!("WASM module compiled in {} ms.", now.elapsed().as_millis());
    Ok(module)
}