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
40	FlushScheduled:AtomicBool,
41}
42
43static COMMAND_EMIT_BATCH:OnceLock<Arc<CommandEmitBatch>> = OnceLock::new();
44
45fn EnqueueCommandEmit(Handle:&AppHandle, Payload:Value) {
46	let Batch = COMMAND_EMIT_BATCH.get_or_init(|| {
47		Arc::new(CommandEmitBatch { Pending:Mutex::new(Vec::new()), FlushScheduled:AtomicBool::new(false) })
48	});
49
50	{
51		let mut Pending = Batch.Pending.lock().unwrap();
52
53		Pending.push(Payload);
54	}
55
56	if !Batch.FlushScheduled.swap(true, Ordering::AcqRel) {
57		let BatchClone = Batch.clone();
58
59		let HandleClone = Handle.clone();
60
61		tokio::spawn(async move {
62			tokio::time::sleep(Duration::from_millis(16)).await;
63			let Drained:Vec<Value> = {
64				let mut Pending = BatchClone.Pending.lock().unwrap();
65				std::mem::take(&mut *Pending)
66			};
67			BatchClone.FlushScheduled.store(false, Ordering::Release);
68			if Drained.is_empty() {
69				return;
70			}
71			let Count = Drained.len();
72			match HandleClone.emit("sky://command/register", json!({ "commands": Drained })) {
73				Ok(()) => {
74					dev_log!("sky-emit", "[SkyEmit] ok channel=sky://command/register batch={}", Count);
75				},
76				Err(Error) => {
77					dev_log!(
78						"sky-emit",
79						"[SkyEmit] fail channel=sky://command/register batch={} error={}",
80						Count,
81						Error
82					);
83				},
84			}
85		});
86	}
87}
88
89pub async fn RegisterCommand(Service:&MountainVinegRPCService, Parameter:&Value) {
90	let CommandId = Parameter.get("commandId").and_then(Value::as_str).unwrap_or("");
91
92	// Per-command registration (~100 commands / session). Useful for
93	// verifying extension command contributions but noisy at the `grpc`
94	// level. Route to `command-register` so it's opt-in alongside
95	// `provider-register`.
96	dev_log!(
97		"command-register",
98		"[MountainVinegRPCService] Cocoon registered command: {}",
99		CommandId
100	);
101
102	if CommandId.is_empty() {
103		return;
104	}
105
106	let Kind = Parameter.get("kind").and_then(Value::as_str).unwrap_or("command").to_string();
107
108	if let Ok(mut Registry) = Service
109		.RunTime()
110		.Environment
111		.ApplicationState
112		.Extension
113		.Registry
114		.CommandRegistry
115		.lock()
116	{
117		Registry.insert(
118			CommandId.to_string(),
119			CommandHandler::Proxied {
120				SideCarIdentifier:"cocoon-main".to_string(),
121				CommandIdentifier:CommandId.to_string(),
122			},
123		);
124	}
125
126	// Coalesce the Sky emit. SkyBridge listens on `sky://command/register`
127	// and accepts either `{ id, commandId, kind }` (single) or
128	// `{ commands: [...] }` (batch). The batched flush happens 16 ms
129	// after the first command lands, so an extension-boot burst of 1000+
130	// registrations becomes a single Tauri emit instead of 1000.
131	EnqueueCommandEmit(
132		Service.ApplicationHandle(),
133		json!({ "id": CommandId, "commandId": CommandId, "kind": Kind }),
134	);
135}