terrazzo-terminal 0.2.7

A simple web-based terminal emulator built on Terrazzo.
#![cfg(feature = "tiles-state")]

macro_rules! make_state {
($name:ident, $ty:ty) => {
    $crate::tiles::state::make_state!($name, $ty, Default::default());
};
($name:ident, $ty:ty, $default:expr) => {
    pub mod $name {
        use server_fn::ServerFnError;
        use terrazzo::server;

        pub mod ty {
            pub type Type = $ty;

            #[allow(unused)]
            pub use super::super::*;
        }

        #[cfg(feature = "server")]
        fn default_value() -> ty::Type {
            #[allow(unused)]
            pub use super::*;

            $default
        }

        #[cfg(feature = "server")]
        mod state {
            use std::collections::HashMap;
            use std::hash::BuildHasherDefault;
            use std::hash::DefaultHasher;
            use crate::tiles::id::TileId;

            pub static STATE: std::sync::Mutex<HashMap<Option<TileId>, super::ty::Type, BuildHasherDefault<DefaultHasher>>> =
                std::sync::Mutex::new(HashMap::with_hasher(BuildHasherDefault::new()));
        }

        use crate::api::client_address::ClientAddress;
        use crate::tiles::id::TileId;

        #[cfg_attr(feature = "server", allow(unused))]
        #[server(protocol = ::server_fn::Http<::server_fn::codec::Json, ::server_fn::codec::Json>)]
        #[cfg_attr(feature = "server", nameth::nameth)]
        pub async fn get(
            tile: Option<TileId>,
            remote: ClientAddress,
        ) -> Result<ty::Type, ServerFnError> {
            Ok(remote::GET_REMOTE_FN
                .call(remote, remote::GetRequest { tile })
                .await?)
        }

        #[cfg(feature = "client")]
        use crate::frontend::remotes::Remote;

        #[cfg(feature = "client")]
        pub async fn set(
            tile: Option<TileId>,
            remote: Remote,
            value: ty::Type,
        ) -> Result<(), ServerFnError> {
            use std::pin::Pin;
            use std::sync::OnceLock;
            use std::time::Duration;
            use futures::future::Shared;
            use terrazzo::prelude::diagnostics::warn;
            use terrazzo::widgets::debounce::DoDebounce as _;

            struct ThreadSafe(
                Box<dyn Fn((Option<TileId>, Remote, ty::Type)) -> Shared<Pin<Box<dyn Future<Output = ()> + Send + Sync>>>>,
            );

            unsafe impl Send for ThreadSafe {}
            unsafe impl Sync for ThreadSafe {}

            static DEBOUNCED_SET: OnceLock<ThreadSafe> = OnceLock::new();
            const STORE_STATE_DEBOUNCE_DELAY: Duration = Duration::from_millis(100);

            let debounced_set = DEBOUNCED_SET.get_or_init(|| {
                ThreadSafe(Box::new(
                    STORE_STATE_DEBOUNCE_DELAY.async_debounce(|(tile, remote, value)| async move {
                        set_impl(tile, remote, value)
                            .await
                            .unwrap_or_else(|error| warn!("Failed to save: {error}"))
                    }),
                ))
            });
            let debounced_set = &*debounced_set.0;

            let () = debounced_set((tile, remote, value)).await;
            Ok(())
        }

        #[cfg_attr(feature = "server", allow(unused))]
        #[server(protocol = ::server_fn::Http<::server_fn::codec::Json, ::server_fn::codec::Json>)]
        #[cfg_attr(feature = "server", nameth::nameth)]
        async fn set_impl(
            tile: Option<TileId>,
            remote: ClientAddress,
            value: ty::Type,
        ) -> Result<(), ServerFnError> {
            Ok(remote::SET_REMOTE_FN
                .call(remote, remote::SetRequest { tile, value })
                .await?)
        }

        #[cfg(feature = "server")]
        mod remote {
            use std::future::ready;

            use const_format::formatcp;
            use serde::Deserialize;
            use serde::Serialize;

            use crate::backend::client_service::remote_fn_service;
            use crate::tiles::id::TileId;

            #[derive(Debug, Default, Serialize, Deserialize)]
            #[serde(default)]
            pub struct GetRequest {
                #[cfg_attr(not(feature = "diagnostics"), serde(rename = "t"))]
                pub(super) tile: Option<TileId>,
            }

            #[derive(Debug, Serialize, Deserialize)]
            #[serde(default)]
            pub struct SetRequest {
                #[cfg_attr(not(feature = "diagnostics"), serde(rename = "t"))]
                pub(super) tile: Option<TileId>,

                #[cfg_attr(not(feature = "diagnostics"), serde(rename = "v"))]
                pub(super) value: super::ty::Type,
            }

            impl Default for SetRequest {
                fn default() -> Self {
                    Self {
                        tile: Default::default(),
                        value: super::default_value(),
                    }
                }
            }

            remote_fn_service::unary::declare_remote_fn!(
                GET_REMOTE_FN,
                formatcp!("{}-state-{}", super::GET, stringify!($name)),
                GetRequest,
                super::ty::Type,
                |_server, request: GetRequest| {
                    let state = super::state::STATE.lock().expect(stringify!($name));
                    ready(Ok::<super::ty::Type, StateError>(
                        state
                            .get(&request.tile)
                            .cloned()
                            .unwrap_or_else(super::default_value),
                    ))
                }
            );

            remote_fn_service::unary::declare_remote_fn!(
                SET_REMOTE_FN,
                formatcp!("{}-state-{}", super::SET_IMPL, stringify!($name)),
                SetRequest,
                (),
                |_server, request: SetRequest| {
                    let mut state = super::state::STATE.lock().expect(stringify!($name));
                    state.insert(request.tile, request.value);
                    ready(Ok::<(), StateError>(()))
                }
            );

            enum StateError {}

            impl From<StateError> for tonic::Status {
                fn from(value: StateError) -> Self {
                    match value {}
                }
            }
        }
    }
};
}

pub(crate) use make_state;