Skip to main content

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()) }