Mountain/Binary/IPC/VineSubscribeCommand.rs
1//! # VineSubscribeCommand
2//!
3//! Tauri command surface that exposes the process-wide Vine
4//! notification broadcast (`Vine::Client::SubscribeNotifications`)
5//! to Sky / Wind via a Tauri Channel<NotificationFramePayload>.
6//!
7//! Wind / Sky subscribers consume each frame as it arrives - same
8//! ordering, same drop-oldest semantics as the in-process Rust
9//! subscribers. The Effect-TS Layer in
10//! `Element/Wind/Source/Effect/Vine/NotificationStream.ts` wraps this
11//! into a `Stream<NotificationFrame>`.
12//!
13//! Frame shape on the wire (serde_json):
14//!
15//! ```json
16//! {
17//! "sideCarIdentifier": "cocoon-main",
18//! "method": "Diagnostic.Set",
19//! "parameters": <payload>,
20//! "timestampNanos": 17775062973342540
21//! }
22//! ```
23
24use serde::Serialize;
25use serde_json::Value;
26use tauri::ipc::Channel;
27
28use crate::{Vine::Client::SubscribeNotifications::Fn as SubscribeNotifications, dev_log};
29
30/// Webview-facing notification frame. Mirror of the Rust
31/// `Vine::Client::NotificationFrame` with camelCase field names per
32/// Land's wire convention. Field renames keep Sky's TS bindings
33/// stable even if the Rust struct evolves.
34#[derive(Debug, Clone, Serialize)]
35#[serde(rename_all = "camelCase")]
36pub struct NotificationFramePayload {
37 pub side_car_identifier:String,
38 pub method:String,
39 pub parameters:Value,
40 pub timestamp_nanos:u64,
41}
42
43/// Subscribe to the Vine notification broadcast. Each call returns a
44/// fresh subscription (own broadcast::Receiver) and spawns a tokio
45/// task that drains the receiver onto the supplied Tauri Channel
46/// until the webview drops it. Drop-oldest at capacity 4096; slow
47/// subscribers may see gaps but never block the producer.
48///
49/// Returns the current subscriber count (post-subscribe) so the
50/// frontend can verify the channel is registered.
51#[tauri::command]
52pub async fn vine_subscribe_notifications(channel:Channel<NotificationFramePayload>) -> Result<usize, String> {
53 let mut Receiver = SubscribeNotifications();
54 let SubscriberCount = crate::Vine::Client::SubscriberCount::Fn();
55 dev_log!(
56 "grpc",
57 "[VineSubscribe] webview subscribed; total_subscribers={}",
58 SubscriberCount
59 );
60
61 tokio::spawn(async move {
62 loop {
63 match Receiver.recv().await {
64 Ok(Frame) => {
65 let Payload = NotificationFramePayload {
66 side_car_identifier:Frame.SideCarIdentifier,
67 method:Frame.Method,
68 parameters:Frame.Parameters,
69 timestamp_nanos:Frame.TimestampNanos,
70 };
71 if let Err(Error) = channel.send(Payload) {
72 // Channel closed - the webview disposed its
73 // subscription. Exit the drain task.
74 dev_log!("grpc", "[VineSubscribe] channel closed ({}); ending drain task", Error);
75 break;
76 }
77 },
78 Err(tokio::sync::broadcast::error::RecvError::Lagged(N)) => {
79 // Subscriber fell behind; drop-oldest semantics.
80 // Surface the gap count so the consumer can decide
81 // whether to refresh state.
82 dev_log!("grpc", "warn: [VineSubscribe] subscriber lagged; dropped {} frames", N);
83 },
84 Err(tokio::sync::broadcast::error::RecvError::Closed) => {
85 // Producer side closed (process shutdown).
86 break;
87 },
88 }
89 }
90 });
91
92 Ok(SubscriberCount)
93}
94
95/// Diagnostic: how many active subscribers exist on the broadcast.
96/// Useful from the frontend for verifying that prior subscriptions
97/// haven't leaked across reloads.
98#[tauri::command]
99pub async fn vine_subscriber_count() -> Result<usize, String> { Ok(crate::Vine::Client::SubscriberCount::Fn()) }