//! `pubsub::` Rhai bridge — durable publish (v1.1.5). //! //! ```rhai //! pubsub::publish_durable("user.created", #{ user_id: "abc" }); //! pubsub::publish_durable("metric", 42); //! ``` //! //! No handle pattern (topics ARE the grouping unit, so there's no //! `::collection(...)`). The message is any JSON-serializable Rhai value //! — Maps, Arrays, strings, numbers, bools, unit, and **Blobs (which //! encode as base64 strings** so trigger handlers see them as base64 on //! the wire). Nested blobs are encoded at any depth. //! //! `app_id` is derived from `cx.app_id` in the service — it never //! appears in the script-side signature, preserving cross-app //! isolation. use std::sync::Arc; use base64::engine::general_purpose::STANDARD; use base64::Engine as _; use picloud_shared::{SdkCallCx, Services}; use rhai::{Array, Dynamic, Engine as RhaiEngine, EvalAltResult, Map, Module}; use serde_json::Value as Json; use tokio::runtime::Handle as TokioHandle; use super::bridge::block_on; pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc) { let svc = services.pubsub.clone(); let mut module = Module::new(); { let svc = svc.clone(); let cx = cx.clone(); module.set_native_fn( "publish_durable", move |topic: &str, message: Dynamic| -> Result<(), Box> { let json = message_to_json(&message); let svc = svc.clone(); let cx = cx.clone(); block_on("pubsub", async move { svc.publish_durable(&cx, topic, json).await }) }, ); } // `pubsub::subscriber_token(topics)` — uses the configured default // TTL. { let svc = svc.clone(); let cx = cx.clone(); module.set_native_fn( "subscriber_token", move |topics: Array| -> Result> { mint_token(&svc, &cx, topics, None) }, ); } // `pubsub::subscriber_token(topics, ttl)` — `ttl` is an integer // (seconds) or `()` for the default. { let svc = svc.clone(); let cx = cx.clone(); module.set_native_fn( "subscriber_token", move |topics: Array, ttl: Dynamic| -> Result> { let ttl = ttl_from_dynamic(&ttl)?; mint_token(&svc, &cx, topics, ttl) }, ); } engine.register_static_module("pubsub", module.into()); } /// Interpret the optional `ttl` argument: `()` → use the default, /// integer → that many seconds, anything else → throw. fn ttl_from_dynamic(ttl: &Dynamic) -> Result, Box> { if ttl.is_unit() { return Ok(None); } ttl.as_int().map(Some).map_err(|_| -> Box { EvalAltResult::ErrorRuntime( "pubsub::subscriber_token: ttl must be an integer (seconds) or ()".into(), rhai::Position::NONE, ) .into() }) } fn mint_token( svc: &Arc, cx: &Arc, topics: Array, ttl: Option, ) -> Result> { // Every element must be a string; surface a clear error otherwise. let mut names = Vec::with_capacity(topics.len()); for t in topics { if !t.is_string() { return Err(EvalAltResult::ErrorRuntime( "pubsub::subscriber_token: topics must be an array of strings".into(), rhai::Position::NONE, ) .into()); } names.push(t.into_string().unwrap_or_default()); } let svc = svc.clone(); let cx = cx.clone(); let handle = TokioHandle::try_current().map_err(|e| -> Box { EvalAltResult::ErrorRuntime( format!("pubsub: no tokio runtime available: {e}").into(), rhai::Position::NONE, ) .into() })?; // SubscriberToken errors already carry the full // "pubsub::subscriber_token: …" wording, so surface them verbatim. handle .block_on(async move { svc.mint_subscriber_token(&cx, names, ttl).await }) .map_err(|err| -> Box { EvalAltResult::ErrorRuntime(format!("{err}").into(), rhai::Position::NONE).into() }) } /// Convert a Rhai `Dynamic` message into JSON, base64-encoding any /// `Blob` (at any nesting depth). Mirrors `bridge::dynamic_to_json` but /// adds the blob arm the pub/sub wire contract requires. fn message_to_json(value: &Dynamic) -> Json { // Blob must be checked before the generic array path (a Blob is a // `Vec`, distinct from a Rhai `Array`). if value.is_blob() { let blob = value.clone().into_blob().unwrap_or_default(); return Json::String(STANDARD.encode(&blob)); } if value.is_unit() { return Json::Null; } if let Ok(b) = value.as_bool() { return Json::Bool(b); } if let Ok(i) = value.as_int() { return Json::Number(i.into()); } if let Ok(f) = value.as_float() { return serde_json::Number::from_f64(f).map_or(Json::Null, Json::Number); } if value.is_string() { return Json::String(value.clone().into_string().unwrap_or_default()); } if let Some(arr) = value.clone().try_cast::() { return Json::Array(arr.iter().map(message_to_json).collect()); } if let Some(map) = value.clone().try_cast::() { let mut out = serde_json::Map::new(); for (k, v) in map { out.insert(k.to_string(), message_to_json(&v)); } return Json::Object(out); } Json::String(value.to_string()) }