Skip to main content

Mountain/Vine/Server/Notification/
UpdateScmGroup.rs

1//! Cocoon → Mountain `update_scm_group` notification.
2//!
3//! Parallels the typed `RPC/CocoonService/SCM.rs::UpdateScmGroup` gRPC;
4//! Cocoon's `ScmNamespace.ts` emits through `SendToMountain(...)` for
5//! fire-and-forget resource-state updates. Re-emits on the canonical
6//! `sky://scm/updateGroup` channel so the renderer SCM view updates
7//! without waiting for a round-trip response.
8//!
9//! Wire shape (from `ScmNamespace.ts:108`):
10//!
11//! ```ignore
12//! { scm_handle: u32, group_handle: "<scm_handle>/<group_id>", resource_states: [...] }
13//! ```
14//!
15//! Earlier revisions of this atom read `provider_id`/`group_id` and
16//! silently dropped every update because Cocoon never sends those keys
17//! - the resulting `[ScmGroup] skip: missing provider_id or group_id`
18//! line was the only signal the SCM viewlet was being starved. The
19//! current decoder reads the canonical handle pair, splits the
20//! `<handle>/<groupId>` form for the renderer payload, and falls back
21//! to the legacy `provider_id`/`group_id` keys for any stale caller
22//! that hasn't migrated yet.
23
24use serde_json::{Value, json};
25use tauri::Emitter;
26
27use crate::{Vine::Server::MountainVinegRPCService::MountainVinegRPCService, dev_log};
28
29pub async fn UpdateScmGroup(Service:&MountainVinegRPCService, Parameter:&Value) {
30	// Producer (Cocoon `ScmNamespace.ts`) emits camelCase keys post-audit.
31	// snake_case probes retained as transitional fallback for one rebuild.
32	let ScmHandle = Parameter
33		.get("scmHandle")
34		.or_else(|| Parameter.get("scm_handle"))
35		.and_then(Value::as_u64)
36		.map(|H| H as u32);
37
38	let GroupHandle = Parameter
39		.get("groupHandle")
40		.or_else(|| Parameter.get("group_handle"))
41		.and_then(Value::as_str)
42		.unwrap_or("")
43		.to_string();
44
45	// Legacy fallbacks: pre-2026-04 Cocoon revisions used flat
46	// `provider_id`/`group_id`. Keep parsing them so a downgrade of
47	// just one side does not silently drop traffic.
48	let LegacyProviderId = Parameter
49		.get("providerId")
50		.or_else(|| Parameter.get("provider_id"))
51		.and_then(Value::as_str)
52		.unwrap_or("")
53		.to_string();
54
55	let LegacyGroupId = Parameter
56		.get("groupId")
57		.or_else(|| Parameter.get("group_id"))
58		.and_then(Value::as_str)
59		.unwrap_or("")
60		.to_string();
61
62	let ResourceStates = Parameter
63		.get("resourceStates")
64		.or_else(|| Parameter.get("resource_states"))
65		.cloned()
66		.unwrap_or_else(|| Value::Array(Vec::new()));
67
68	// `group_handle` is `"<scm_handle>/<group_id>"` per ScmNamespace.ts:77.
69	// Split for the renderer payload so the existing
70	// `cel:scm:updateGroup` listeners (which expect a flat `groupId`)
71	// keep working without forcing them to re-parse.
72	let (HandleFromString, GroupIdFromHandle) = match GroupHandle.split_once('/') {
73		Some((H, G)) => (H.parse::<u32>().ok(), G.to_string()),
74
75		None => (None, String::new()),
76	};
77
78	let ResolvedScmHandle = ScmHandle.or(HandleFromString);
79
80	let ResolvedGroupId = if !GroupIdFromHandle.is_empty() {
81		GroupIdFromHandle
82	} else if !LegacyGroupId.is_empty() {
83		LegacyGroupId
84	} else {
85		String::new()
86	};
87
88	if ResolvedScmHandle.is_none() && LegacyProviderId.is_empty() {
89		dev_log!(
90			"grpc",
91			"[ScmGroup] skip: missing scm_handle / provider_id (group_handle={:?} legacy_group={:?})",
92			GroupHandle,
93			ResolvedGroupId
94		);
95
96		return;
97	}
98
99	if ResolvedGroupId.is_empty() {
100		dev_log!(
101			"grpc",
102			"[ScmGroup] skip: missing group_id (scm_handle={:?} group_handle={:?})",
103			ResolvedScmHandle,
104			GroupHandle
105		);
106
107		return;
108	}
109
110	let _ = Service.ApplicationHandle().emit(
111		"sky://scm/updateGroup",
112		json!({
113			"scmHandle": ResolvedScmHandle,
114			"providerId": &LegacyProviderId,
115			"groupHandle": &GroupHandle,
116			"groupId": &ResolvedGroupId,
117			"resourceStates": ResourceStates,
118		}),
119	);
120
121	dev_log!(
122		"grpc",
123		"[ScmGroup] scm_handle={:?} group={} resources={}",
124		ResolvedScmHandle,
125		ResolvedGroupId,
126		ResourceStates.as_array().map(Vec::len).unwrap_or(0)
127	);
128}