Skip to main content

lattice_plugin_host/
multibuffer_view_task.rs

1//! MV.1 — the per-plugin actor bridge for multibuffer-view sources.
2//!
3//! The `picker_task.rs` shape, and deliberately so: a dedicated async task owns
4//! the plugin's `Store<PluginState>` for life (the `Store` is `!Sync`), a
5//! [`ViewCall`] crosses an mpsc channel with a `oneshot` reply, and the
6//! `Send + Sync` [`MultibufferViewClient`] serializes calls onto the single-
7//! consumer loop. [`PluginHost::spawn_multibuffer_view_source`] instantiates the
8//! `multibuffer-view-plugin` world under the plugin's grant and returns
9//! `(client, actor)`; the caller drives [`MultibufferViewActor::run`] on its
10//! multi-thread runtime (the lib owns no runtime).
11//!
12//! This is the fourth actor of this shape (picker, completion, agenda, view).
13//! The rule-of-three trigger to generalise the loop over the bindings type has
14//! fired, and generalising it is its own refactor rather than a thing to attempt
15//! inside the slice that adds the fourth — noted here so the next one does not
16//! have to rediscover the count.
17
18use std::sync::Arc;
19
20use futures::StreamExt;
21use futures::channel::{mpsc, oneshot};
22use lattice_runtime::EventBus;
23use wasmtime::Store;
24
25use crate::multibuffer_view_host::bindings::MultibufferViewPlugin;
26use crate::{
27    Component, PluginBudget, PluginHost, PluginHostError, PluginId, PluginManifest, PluginState,
28    TrustTier, arm_store,
29};
30
31pub use crate::lattice::plugin_host::types::{MultibufferViewResult, MultibufferViewSpec};
32
33/// See `picker_task::CallResult`.
34type CallResult<T> = Result<T, PluginHostError>;
35
36/// A request sent from a [`MultibufferViewClient`] to its actor.
37enum ViewCall {
38    /// `register-multibuffer-views()` — drive the guest's registration export,
39    /// then hand back every view it declared through the imported
40    /// `register-multibuffer-view`.
41    RegisterViews {
42        reply: oneshot::Sender<CallResult<Vec<MultibufferViewSpec>>>,
43    },
44    /// `multibuffer-view-source.build(view, args)` — produce one view's
45    /// excerpts. Replies the guest's `result<multibuffer-view-result, string>`
46    /// (or a host trap).
47    Build {
48        view: String,
49        args: Vec<String>,
50        reply: oneshot::Sender<CallResult<Result<MultibufferViewResult, String>>>,
51    },
52}
53
54/// The `Send + Sync` handle the provider holds. Cloning is cheap (an mpsc
55/// `Sender` clone); every clone talks to the same actor / `Store`, so calls
56/// serialize on the single-consumer loop — the guarantee the `!Sync` `Store`
57/// needs. Dropping the last clone ends the actor loop (teardown).
58#[derive(Clone)]
59pub struct MultibufferViewClient {
60    tx: mpsc::UnboundedSender<ViewCall>,
61    id: PluginId,
62}
63
64impl MultibufferViewClient {
65    /// The host-issued identity of the plugin behind this client.
66    pub fn id(&self) -> PluginId {
67        self.id
68    }
69
70    /// Drive the guest's `register-multibuffer-views()` and collect every view
71    /// it declared.
72    ///
73    /// An empty list is not an error: a plugin that provides the seam and
74    /// declares nothing registers nothing, which is what it asked for.
75    pub async fn register_views(&self) -> CallResult<Vec<MultibufferViewSpec>> {
76        let (reply, rx) = oneshot::channel();
77        self.dispatch(
78            ViewCall::RegisterViews { reply },
79            rx,
80            "register-multibuffer-views",
81        )
82        .await
83    }
84
85    /// Call the guest's `build(view, args)`. The outer result is the host
86    /// surface; the inner `Result<_, String>` is the guest's own WIT `result`,
87    /// whose `Err` **declines** the view with the guest's message rather than
88    /// opening an empty one.
89    pub async fn build(
90        &self,
91        view: String,
92        args: Vec<String>,
93    ) -> CallResult<Result<MultibufferViewResult, String>> {
94        let (reply, rx) = oneshot::channel();
95        self.dispatch(ViewCall::Build { view, args, reply }, rx, "build")
96            .await
97    }
98
99    /// Shared send-then-await-reply. A closed channel or a dropped reply sender
100    /// both surface as [`PluginGone`](PluginHostError::PluginGone) — the caller
101    /// stays live.
102    async fn dispatch<T>(
103        &self,
104        call: ViewCall,
105        rx: oneshot::Receiver<CallResult<T>>,
106        func: &'static str,
107    ) -> CallResult<T> {
108        self.tx
109            .unbounded_send(call)
110            .map_err(|_| PluginHostError::PluginGone { func })?;
111        rx.await.map_err(|_| PluginHostError::PluginGone { func })?
112    }
113}
114
115/// The per-plugin actor: owns the `Store` + view bindings for the plugin's whole
116/// life and serves calls until every client is dropped.
117pub struct MultibufferViewActor {
118    store: Store<PluginState>,
119    bindings: MultibufferViewPlugin,
120    budget: PluginBudget,
121    rx: mpsc::UnboundedReceiver<ViewCall>,
122    id: PluginId,
123    quarantine: crate::Quarantine,
124    tracer: Option<crate::trace::PluginTracerHandle>,
125}
126
127impl MultibufferViewActor {
128    /// The host-issued identity of this plugin.
129    pub fn id(&self) -> PluginId {
130        self.id
131    }
132
133    /// PO.2: attach the boundary tracer before spawning `run`.
134    pub fn with_tracer(mut self, tracer: Option<crate::trace::PluginTracerHandle>) -> Self {
135        self.tracer = tracer;
136        self
137    }
138
139    /// Drive the actor to completion. A trap does not end the loop — the
140    /// `Store` survives a clean fuel/epoch trap, and quarantine handles the
141    /// rest. The loop ends when the channel closes, dropping the `Store`.
142    pub async fn run(mut self) {
143        while let Some(call) = self.rx.next().await {
144            match call {
145                ViewCall::RegisterViews { reply } => {
146                    let _ = reply.send(self.call_register_views().await);
147                }
148                ViewCall::Build { view, args, reply } => {
149                    let _ = reply.send(self.call_build(&view, &args).await);
150                }
151            }
152        }
153    }
154
155    /// Drive `register-multibuffer-views`, then drain what the guest declared.
156    ///
157    /// The drain reads `PluginState` AFTER the export returns — the
158    /// `register-grammar` shape, because a guest registers by *calling*, so the
159    /// specs do not exist until its body has run.
160    async fn call_register_views(&mut self) -> CallResult<Vec<MultibufferViewSpec>> {
161        if self.quarantine.is_tripped() {
162            return Err(PluginHostError::Quarantined {
163                func: "register-multibuffer-views",
164            });
165        }
166        arm_store(&mut self.store, self.budget)?;
167        let __trace_start = std::time::Instant::now();
168        let result = self
169            .bindings
170            .call_register_multibuffer_views(&mut self.store)
171            .await;
172        crate::trip_and_map_traced(
173            self.tracer.as_ref(),
174            self.id.0,
175            crate::PluginSeam::MultibufferViewSource,
176            &mut self.quarantine,
177            "register-multibuffer-views",
178            __trace_start,
179            result,
180        )?;
181        Ok(std::mem::take(
182            &mut self.store.data_mut().multibuffer_view_contributions.specs,
183        ))
184    }
185
186    async fn call_build(
187        &mut self,
188        view: &str,
189        args: &[String],
190    ) -> CallResult<Result<MultibufferViewResult, String>> {
191        if self.quarantine.is_tripped() {
192            return Err(PluginHostError::Quarantined { func: "build" });
193        }
194        arm_store(&mut self.store, self.budget)?;
195        let __trace_start = std::time::Instant::now();
196        let result = self
197            .bindings
198            .lattice_plugin_host_multibuffer_view_source()
199            .call_build(&mut self.store, view, args)
200            .await;
201        crate::trip_and_map_traced(
202            self.tracer.as_ref(),
203            self.id.0,
204            crate::PluginSeam::MultibufferViewSource,
205            &mut self.quarantine,
206            "build",
207            __trace_start,
208            result,
209        )
210    }
211}
212
213impl PluginHost {
214    /// Instantiate a `multibuffer-view-plugin` component under its capability
215    /// grant and return the bridge. Grant / data-dir / WASI are identical to
216    /// `instantiate_plugin`; the actor is *not* spawned here (the lib owns no
217    /// runtime). Mirror of [`spawn_picker_source`](Self::spawn_picker_source).
218    pub async fn spawn_multibuffer_view_source(
219        &self,
220        component: &Component,
221        manifest: &PluginManifest,
222        tier: TrustTier,
223        budget: PluginBudget,
224        bus: &Arc<EventBus>,
225        config: Option<&Arc<lattice_config::ConfigRegistry>>,
226    ) -> Result<(MultibufferViewClient, MultibufferViewActor), PluginHostError> {
227        let (wasi, outcome, _data_dir) = self.build_plugin_wasi(manifest, tier);
228        for denied in &outcome.denied {
229            tracing::warn!(
230                plugin = %manifest.id,
231                capability = ?denied,
232                "multibuffer-view plugin loaded with a withheld capability (reduced function)"
233            );
234        }
235        let mut store = self.new_store(wasi, outcome.grant, budget, Some(&manifest.id))?;
236        let bindings =
237            MultibufferViewPlugin::instantiate_async(&mut store, component, &self.linker)
238                .await
239                .map_err(|e| PluginHostError::Instantiate(e.into()))?;
240        let id = self.alloc_id();
241        store.data_mut().log_ctx = self.log_ctx_for(id);
242        // MV.1: the config registry. The SEVENTH seam to need this line, and six
243        // of the previous ones shipped without it — each answering `none` to
244        // `get-option` while looking perfectly wired. A view's contents very
245        // often depend on an option (which directory, which filter), so it is
246        // stamped here rather than waiting for the bug report.
247        if let Some(registry) = config {
248            store.data_mut().config_registry = Some(Arc::clone(registry));
249        }
250        let (tx, rx) = mpsc::unbounded();
251        let client = MultibufferViewClient { tx, id };
252        let actor = MultibufferViewActor {
253            store,
254            bindings,
255            budget,
256            rx,
257            id,
258            quarantine: crate::Quarantine::new(id, Arc::clone(bus)),
259            tracer: None,
260        };
261        Ok((client, actor))
262    }
263}