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
39 pub method:String,
40
41 pub parameters:Value,
42
43 pub timestamp_nanos:u64,
44}
45
46/// Subscribe to the Vine notification broadcast. Each call returns a
47/// fresh subscription (own broadcast::Receiver) and spawns a tokio
48/// task that drains the receiver onto the supplied Tauri Channel
49/// until the webview drops it. Drop-oldest at capacity 4096; slow
50/// subscribers may see gaps but never block the producer.
51///
52/// Returns the current subscriber count (post-subscribe) so the
53/// frontend can verify the channel is registered.
54#[tauri::command]
55pub async fn vine_subscribe_notifications(channel:Channel<NotificationFramePayload>) -> Result<usize, String> {
56 let mut Receiver = SubscribeNotifications();
57
58 let SubscriberCount = crate::Vine::Client::SubscriberCount::Fn();
59
60 dev_log!(
61 "grpc",
62 "[VineSubscribe] webview subscribed; total_subscribers={}",
63 SubscriberCount
64 );
65
66 tokio::spawn(async move {
67 loop {
68 match Receiver.recv().await {
69 Ok(Frame) => {
70 let Payload = NotificationFramePayload {
71 side_car_identifier:Frame.SideCarIdentifier,
72 method:Frame.Method,
73 parameters:Frame.Parameters,
74 timestamp_nanos:Frame.TimestampNanos,
75 };
76 if let Err(Error) = channel.send(Payload) {
77 // Channel closed - the webview disposed its
78 // subscription. Exit the drain task.
79 dev_log!("grpc", "[VineSubscribe] channel closed ({}); ending drain task", Error);
80 break;
81 }
82 },
83 Err(tokio::sync::broadcast::error::RecvError::Lagged(N)) => {
84 // Subscriber fell behind; drop-oldest semantics.
85 // Surface the gap count so the consumer can decide
86 // whether to refresh state.
87 dev_log!("grpc", "warn: [VineSubscribe] subscriber lagged; dropped {} frames", N);
88 },
89 Err(tokio::sync::broadcast::error::RecvError::Closed) => {
90 // Producer side closed (process shutdown).
91 break;
92 },
93 }
94 }
95 });
96
97 Ok(SubscriberCount)
98}
99
100/// Diagnostic: how many active subscribers exist on the broadcast.
101/// Useful from the frontend for verifying that prior subscriptions
102/// haven't leaked across reloads.
103#[tauri::command]
104pub async fn vine_subscriber_count() -> Result<usize, String> { Ok(crate::Vine::Client::SubscriberCount::Fn()) }