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	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	// Legacy fallbacks: pre-2026-04 Cocoon revisions used flat
45	// `provider_id`/`group_id`. Keep parsing them so a downgrade of
46	// just one side does not silently drop traffic.
47	let LegacyProviderId = Parameter
48		.get("providerId")
49		.or_else(|| Parameter.get("provider_id"))
50		.and_then(Value::as_str)
51		.unwrap_or("")
52		.to_string();
53	let LegacyGroupId = Parameter
54		.get("groupId")
55		.or_else(|| Parameter.get("group_id"))
56		.and_then(Value::as_str)
57		.unwrap_or("")
58		.to_string();
59	let ResourceStates = Parameter
60		.get("resourceStates")
61		.or_else(|| Parameter.get("resource_states"))
62		.cloned()
63		.unwrap_or_else(|| Value::Array(Vec::new()));
64
65	// `group_handle` is `"<scm_handle>/<group_id>"` per ScmNamespace.ts:77.
66	// Split for the renderer payload so the existing
67	// `cel:scm:updateGroup` listeners (which expect a flat `groupId`)
68	// keep working without forcing them to re-parse.
69	let (HandleFromString, GroupIdFromHandle) = match GroupHandle.split_once('/') {
70		Some((H, G)) => (H.parse::<u32>().ok(), G.to_string()),
71		None => (None, String::new()),
72	};
73	let ResolvedScmHandle = ScmHandle.or(HandleFromString);
74	let ResolvedGroupId = if !GroupIdFromHandle.is_empty() {
75		GroupIdFromHandle
76	} else if !LegacyGroupId.is_empty() {
77		LegacyGroupId
78	} else {
79		String::new()
80	};
81
82	if ResolvedScmHandle.is_none() && LegacyProviderId.is_empty() {
83		dev_log!(
84			"grpc",
85			"[ScmGroup] skip: missing scm_handle / provider_id (group_handle={:?} legacy_group={:?})",
86			GroupHandle,
87			ResolvedGroupId
88		);
89		return;
90	}
91	if ResolvedGroupId.is_empty() {
92		dev_log!(
93			"grpc",
94			"[ScmGroup] skip: missing group_id (scm_handle={:?} group_handle={:?})",
95			ResolvedScmHandle,
96			GroupHandle
97		);
98		return;
99	}
100
101	let _ = Service.ApplicationHandle().emit(
102		"sky://scm/updateGroup",
103		json!({
104			"scmHandle": ResolvedScmHandle,
105			"providerId": &LegacyProviderId,
106			"groupHandle": &GroupHandle,
107			"groupId": &ResolvedGroupId,
108			"resourceStates": ResourceStates,
109		}),
110	);
111	dev_log!(
112		"grpc",
113		"[ScmGroup] scm_handle={:?} group={} resources={}",
114		ResolvedScmHandle,
115		ResolvedGroupId,
116		ResourceStates.as_array().map(Vec::len).unwrap_or(0)
117	);
118}