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