Skip to main content

Mountain/Vine/Server/Notification/
RegisterCommand.rs

1#![allow(non_snake_case)]
2//! Cocoon → Mountain `registerCommand` notification.
3//! Stores the command as a `Proxied` handler in Mountain's
4//! `CommandRegistry` so subsequent `commands.executeCommand` calls get
5//! routed back to Cocoon via `$executeContributedCommand` gRPC. The
6//! sidecar identifier is hard-coded to `cocoon-main` because that is
7//! the sole extension-host Cocoon instance today.
8
9use std::{
10	sync::{
11		Arc,
12		Mutex,
13		OnceLock,
14		atomic::{AtomicBool, Ordering},
15	},
16	time::Duration,
17};
18
19use serde_json::{Value, json};
20use tauri::{AppHandle, Emitter};
21
22use crate::{
23	Environment::CommandProvider::CommandHandler,
24	Vine::Server::MountainVinegRPCService::MountainVinegRPCService,
25	dev_log,
26};
27
28/// Coalesced Mountain → Sky emit buffer for `sky://command/register`.
29///
30/// Extension boot fires 1000+ `registerCommand` notifications in a
31/// tight burst (113 extensions × ~10 commands each). Emitting one
32/// Tauri event per command saturated the WKWebView IPC channel that
33/// also carries keystroke delivery; users could type for a split
34/// second before the burst hit, then nothing. Buffer for one frame
35/// (16 ms) and emit a single `{ commands: [...] }` batch instead.
36/// SkyBridge's listener accepts both shapes (single + batch).
37struct CommandEmitBatch {
38	Pending:Mutex<Vec<Value>>,
39	FlushScheduled:AtomicBool,
40}
41
42static COMMAND_EMIT_BATCH:OnceLock<Arc<CommandEmitBatch>> = OnceLock::new();
43
44fn EnqueueCommandEmit(Handle:&AppHandle, Payload:Value) {
45	let Batch = COMMAND_EMIT_BATCH.get_or_init(|| {
46		Arc::new(CommandEmitBatch { Pending:Mutex::new(Vec::new()), FlushScheduled:AtomicBool::new(false) })
47	});
48
49	{
50		let mut Pending = Batch.Pending.lock().unwrap();
51		Pending.push(Payload);
52	}
53
54	if !Batch.FlushScheduled.swap(true, Ordering::AcqRel) {
55		let BatchClone = Batch.clone();
56		let HandleClone = Handle.clone();
57		tokio::spawn(async move {
58			tokio::time::sleep(Duration::from_millis(16)).await;
59			let Drained:Vec<Value> = {
60				let mut Pending = BatchClone.Pending.lock().unwrap();
61				std::mem::take(&mut *Pending)
62			};
63			BatchClone.FlushScheduled.store(false, Ordering::Release);
64			if Drained.is_empty() {
65				return;
66			}
67			let Count = Drained.len();
68			match HandleClone.emit("sky://command/register", json!({ "commands": Drained })) {
69				Ok(()) => {
70					dev_log!("sky-emit", "[SkyEmit] ok channel=sky://command/register batch={}", Count);
71				},
72				Err(Error) => {
73					dev_log!(
74						"sky-emit",
75						"[SkyEmit] fail channel=sky://command/register batch={} error={}",
76						Count,
77						Error
78					);
79				},
80			}
81		});
82	}
83}
84
85pub async fn RegisterCommand(Service:&MountainVinegRPCService, Parameter:&Value) {
86	let CommandId = Parameter.get("commandId").and_then(Value::as_str).unwrap_or("");
87	// Per-command registration (~100 commands / session). Useful for
88	// verifying extension command contributions but noisy at the `grpc`
89	// level. Route to `command-register` so it's opt-in alongside
90	// `provider-register`.
91	dev_log!(
92		"command-register",
93		"[MountainVinegRPCService] Cocoon registered command: {}",
94		CommandId
95	);
96	if CommandId.is_empty() {
97		return;
98	}
99	let Kind = Parameter.get("kind").and_then(Value::as_str).unwrap_or("command").to_string();
100	if let Ok(mut Registry) = Service
101		.RunTime()
102		.Environment
103		.ApplicationState
104		.Extension
105		.Registry
106		.CommandRegistry
107		.lock()
108	{
109		Registry.insert(
110			CommandId.to_string(),
111			CommandHandler::Proxied {
112				SideCarIdentifier:"cocoon-main".to_string(),
113				CommandIdentifier:CommandId.to_string(),
114			},
115		);
116	}
117	// Coalesce the Sky emit. SkyBridge listens on `sky://command/register`
118	// and accepts either `{ id, commandId, kind }` (single) or
119	// `{ commands: [...] }` (batch). The batched flush happens 16 ms
120	// after the first command lands, so an extension-boot burst of 1000+
121	// registrations becomes a single Tauri emit instead of 1000.
122	EnqueueCommandEmit(
123		Service.ApplicationHandle(),
124		json!({ "id": CommandId, "commandId": CommandId, "kind": Kind }),
125	);
126}