~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
180 lines · 5.3 KB Raw History
//! Server-side host for atlibs wasm modules: the Rust twin of javascript/src/server and
//! javascript/src/run-module. Same modules, same step protocol, same host
//! calls, same state dir.

pub mod compose;
pub mod crypto;
pub mod host;
pub mod oauth;
pub mod oauth_client;
pub mod params;
pub mod registry;
pub mod run;
pub mod serviceauth;
pub mod signin;
pub mod store;
pub mod transport;
pub mod wasm;
pub mod ws;

use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

use serde_json::Value;
use tokio::sync::watch;

/// The process's rustls provider (ring), chosen once: atproto-crates' reqwest brings aws-lc-rs into the graph
/// beside ring, and then rustls cannot pick one for the clients that ask the process default (wss subscribes).
pub fn tls() {
    let _ = rustls::crypto::ring::default_provider().install_default();
}

pub type BoxFut<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;

/// An AbortSignal: aborted once, with a reason (the JS Error's message).
#[derive(Clone)]
pub struct Signal(Arc<watch::Sender<Option<String>>>);

impl Default for Signal {
    fn default() -> Self {
        Self(Arc::new(watch::channel(None).0))
    }
}

impl Signal {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn abort(&self, reason: impl Into<String>) {
        let reason = reason.into();
        self.0
            .send_if_modified(|value| value.is_none() && value.replace(reason).is_none());
    }
    pub fn reason(&self) -> Option<String> {
        self.0.borrow().clone()
    }
    pub async fn aborted(&self) -> String {
        let mut rx = self.0.subscribe();
        loop {
            if let Some(reason) = rx.borrow_and_update().clone() {
                return reason;
            }
            let _ = rx.changed().await;
        }
    }
    /// AbortSignal.any([a, b]): aborts with whichever reason comes first.
    pub fn any(a: &Signal, b: &Signal) -> Signal {
        let out = Signal::new();
        if let Some(reason) = a.reason().or_else(|| b.reason()) {
            out.abort(reason);
            return out;
        }
        let (a, b, o) = (a.clone(), b.clone(), out.clone());
        tokio::spawn(async move {
            let reason = tokio::select! { r = a.aborted() => r, r = b.aborted() => r };
            o.abort(reason);
        });
        out
    }
}

/// `new Date(ms).toISOString()`.
pub fn iso(ms: u64) -> String {
    let (secs, milli) = (ms / 1000, ms % 1000);
    let (days, sod) = ((secs / 86400) as i64, secs % 86400);
    let z = days + 719_468;
    let era = z.div_euclid(146_097);
    let doe = z - era * 146_097;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = yoe + era * 400 + i64::from(m <= 2);
    format!(
        "{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}.{milli:03}Z",
        sod / 3600,
        sod % 3600 / 60,
        sod % 60
    )
}

pub fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_or(0, |d| d.as_millis() as u64)
}

pub fn iso_now() -> String {
    iso(now_ms())
}

/// A JS number as `String(n)` prints it.
// ponytail: integers and plain decimals; JS switches to exponent form below 1e-6 and from 1e21.
pub fn js_num(n: f64) -> String {
    if n.fract() == 0.0 && n.abs() < 1e21 {
        format!("{}", n as i128)
    } else {
        format!("{n}")
    }
}

/// `String(value)` for a JSON value; `None` is undefined.
pub fn js_string(value: Option<&Value>) -> String {
    match value {
        None => "undefined".into(),
        Some(Value::Null) => "null".into(),
        Some(Value::Bool(b)) => b.to_string(),
        Some(Value::Number(n)) => n
            .as_i64()
            .map(|i| i.to_string())
            .or_else(|| n.as_u64().map(|u| u.to_string()))
            .unwrap_or_else(|| js_num(n.as_f64().unwrap_or(f64::NAN))),
        Some(Value::String(s)) => s.clone(),
        Some(Value::Array(items)) => items
            .iter()
            .map(|v| {
                if v.is_null() {
                    String::new()
                } else {
                    js_string(Some(v))
                }
            })
            .collect::<Vec<_>>()
            .join(","),
        Some(Value::Object(_)) => "[object Object]".into(),
    }
}

/// `JSON.stringify(value, null, "\t")`.
pub fn pretty(value: &Value) -> String {
    use serde::Serialize;
    let mut out = Vec::new();
    let mut ser = serde_json::Serializer::with_formatter(
        &mut out,
        serde_json::ser::PrettyFormatter::with_indent(b"\t"),
    );
    value.serialize(&mut ser).expect("JSON values serialize");
    String::from_utf8(out).expect("JSON is UTF-8")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn iso_matches_to_iso_string() {
        assert_eq!(iso(0), "1970-01-01T00:00:00.000Z");
        assert_eq!(iso(1_794_573_296_789), "2026-11-13T12:34:56.789Z");
        assert_eq!(iso(951_782_400_000), "2000-02-29T00:00:00.000Z");
    }

    #[test]
    fn js_string_matches_string() {
        let v: Value = serde_json::json!([1, null, "a", [2, 3], {"x": 1}, 1.5, true]);
        assert_eq!(js_string(Some(&v)), "1,,a,2,3,[object Object],1.5,true");
        assert_eq!(js_string(None), "undefined");
        assert_eq!(js_num(2.0), "2");
    }
}