Skip to main content

Mountain/Vine/Server/Notification/
UpdateScmGroup.rs

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