//! `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::{PubsubError, SdkCallCx, Services}; use rhai::{Array, Dynamic, Engine as RhaiEngine, EvalAltResult, Map, Module}; use serde_json::Value as Json; use tokio::runtime::Handle as TokioHandle; 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(async move { svc.publish_durable(&cx, topic, json).await }) }, ); } engine.register_static_module("pubsub", module.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()) } /// Run an async future inside the synchronous Rhai context. Mirrors /// `kv::block_on`. fn block_on(fut: F) -> Result<(), Box> where F: std::future::Future> + Send, { let handle = TokioHandle::try_current().map_err(|e| -> Box { EvalAltResult::ErrorRuntime( format!("pubsub: no tokio runtime available: {e}").into(), rhai::Position::NONE, ) .into() })?; handle.block_on(fut).map_err(|err| -> Box { EvalAltResult::ErrorRuntime(format!("pubsub: {err}").into(), rhai::Position::NONE).into() }) }