Skip to main content

lattice_plugin_host/
decoration_task.rs

1//! PH7.9b — the per-plugin actor bridge for decoration providers.
2//!
3//! The decoration analogue of `completion_task.rs`: a dedicated async task owns
4//! the plugin's `Store<PluginState>` for life (the Store is `!Sync`), a
5//! `DecorationCall` crosses an mpsc channel with a `oneshot` reply, and the
6//! `Send + Sync` [`DecorationClient`] serializes calls onto the single-consumer
7//! loop. `PluginHost::spawn_decoration_source` instantiates the
8//! `decorations-plugin` world under the plugin's grant and returns
9//! `(DecorationClient, DecorationActor)`; the caller drives
10//! [`DecorationActor::run`] on its multi-thread runtime (the lib owns no runtime).
11//!
12//! Like completion (PH7.6), this is a **producer**, host-called OFF the render
13//! path — the host calls `produce` on a trigger (edit / scroll / diagnostic
14//! change), caches the result, and the renderer reads the cache (never WASM on
15//! the tick, paramount #1). The picker / completion / decoration actors are
16//! near-identical request/reply bridges; generalising the loop over the bindings
17//! type is deferred until a real need (the `completion_task` rule-of-three note).
18
19use std::sync::Arc;
20
21use futures::StreamExt;
22use futures::channel::{mpsc, oneshot};
23use lattice_runtime::EventBus;
24use wasmtime::Store;
25
26use crate::decoration_host::bindings::DecorationsPlugin;
27use crate::{
28    Component, PluginBudget, PluginHost, PluginHostError, PluginId, PluginManifest, PluginState,
29    TrustTier, arm_store,
30};
31
32// The decoration WIT records the bridge's public API traffics in — the
33// `with:`-mapped `types` mirrors (`decoration_host.rs`), i.e. the SAME Rust types
34// `WitBoundary` round-trips; the native↔WIT conversion is the caller's job
35// (`boundary_decoration.rs`). Re-exported `pub` (they appear in the
36// `DecorationClient` method signatures).
37pub use crate::lattice::plugin_host::types::{DecorationContext, GutterDecoration};
38
39/// See `completion_task::CallResult`.
40type CallResult<T> = Result<T, PluginHostError>;
41
42/// A request sent from a [`DecorationClient`] to its [`DecorationActor`].
43enum DecorationCall {
44    /// `decorations.gutter-decorations(ctx)` — produce the per-line gutter
45    /// decorations for a buffer. Replies the guest's `result<list<gutter-
46    /// decoration>, string>` (or a host trap).
47    Produce {
48        ctx: Box<DecorationContext>,
49        reply: oneshot::Sender<CallResult<Result<Vec<GutterDecoration>, String>>>,
50    },
51}
52
53/// The `Send + Sync` handle a caller holds. Cloning is cheap (an mpsc `Sender`
54/// clone); every clone talks to the same actor / `Store`, so calls serialize on
55/// the single-consumer loop the `!Sync` `Store` needs.
56#[derive(Clone, Debug)]
57pub struct DecorationClient {
58    tx: mpsc::UnboundedSender<DecorationCall>,
59    id: PluginId,
60}
61
62impl DecorationClient {
63    /// The host-issued identity of the plugin behind this client.
64    pub fn id(&self) -> PluginId {
65        self.id
66    }
67
68    /// Call the guest's `gutter-decorations(ctx)`. The outer result is the host
69    /// surface; the inner `Result<_, String>` is the guest's own WIT `result` (an
70    /// `Err` string is a provider that produced nothing for this trigger — logged,
71    /// the cached snapshot keeps its prior value).
72    pub async fn produce(
73        &self,
74        ctx: DecorationContext,
75    ) -> CallResult<Result<Vec<GutterDecoration>, String>> {
76        let (reply, rx) = oneshot::channel();
77        self.tx
78            .unbounded_send(DecorationCall::Produce {
79                ctx: Box::new(ctx),
80                reply,
81            })
82            .map_err(|_| PluginHostError::PluginGone {
83                func: "gutter-decorations",
84            })?;
85        rx.await.map_err(|_| PluginHostError::PluginGone {
86            func: "gutter-decorations",
87        })?
88    }
89}
90
91/// The per-plugin actor: owns the `Store` + decoration bindings for the plugin's
92/// life and serves calls off the channel until every [`DecorationClient`] drops.
93pub struct DecorationActor {
94    store: Store<PluginState>,
95    bindings: DecorationsPlugin,
96    budget: PluginBudget,
97    rx: mpsc::UnboundedReceiver<DecorationCall>,
98    id: PluginId,
99    /// Crash-quarantine (PH7.12): the first `gutter-decorations` trap trips this,
100    /// fires one `PluginCrashed`, and every later call returns `Quarantined`.
101    quarantine: crate::Quarantine,
102    /// PO.2: the boundary tracer, wired by the loader via with_tracer; None in tests / pre-wire.
103    tracer: Option<crate::trace::PluginTracerHandle>,
104}
105
106impl DecorationActor {
107    /// The host-issued identity of this plugin.
108    pub fn id(&self) -> PluginId {
109        self.id
110    }
111
112    /// PO.2: attach the boundary tracer (the loader calls this before spawning
113    /// run()). Off the hot path — the seam is async.
114    pub fn with_tracer(mut self, tracer: Option<crate::trace::PluginTracerHandle>) -> Self {
115        self.tracer = tracer;
116        self
117    }
118
119    /// Drive the actor to completion — see `completion_task::CompletionActor::run`.
120    pub async fn run(mut self) {
121        while let Some(call) = self.rx.next().await {
122            match call {
123                DecorationCall::Produce { ctx, reply } => {
124                    let _ = reply.send(self.call_produce(&ctx).await);
125                }
126            }
127        }
128    }
129
130    async fn call_produce(
131        &mut self,
132        ctx: &DecorationContext,
133    ) -> CallResult<Result<Vec<GutterDecoration>, String>> {
134        if self.quarantine.is_tripped() {
135            return Err(PluginHostError::Quarantined {
136                func: "gutter-decorations",
137            });
138        }
139        arm_store(&mut self.store, self.budget)?;
140        let __trace_start = std::time::Instant::now();
141        let result = self
142            .bindings
143            .lattice_plugin_host_decorations()
144            .call_gutter_decorations(&mut self.store, ctx)
145            .await;
146        crate::trip_and_map_traced(
147            self.tracer.as_ref(),
148            self.id.0,
149            crate::PluginSeam::Decorations,
150            &mut self.quarantine,
151            "gutter-decorations",
152            __trace_start,
153            result,
154        )
155    }
156}
157
158impl PluginHost {
159    /// Instantiate a `decorations-plugin` component under its capability grant and
160    /// return the bridge: a `Send + Sync` [`DecorationClient`] plus the
161    /// [`DecorationActor`] the caller drives. Grant / data-dir / WASI are identical
162    /// to `instantiate_plugin` (shared `build_plugin_wasi` + `new_store`), and the
163    /// actor is *not* spawned here (the lib owns no runtime). Mirror of
164    /// [`spawn_completion_source`](Self::spawn_completion_source).
165    pub async fn spawn_decoration_source(
166        &self,
167        component: &Component,
168        manifest: &PluginManifest,
169        tier: TrustTier,
170        budget: PluginBudget,
171        bus: &Arc<EventBus>,
172    ) -> Result<(DecorationClient, DecorationActor), PluginHostError> {
173        let (wasi, outcome, _data_dir) = self.build_plugin_wasi(manifest, tier);
174        for denied in &outcome.denied {
175            tracing::warn!(
176                plugin = %manifest.id,
177                capability = ?denied,
178                "decoration plugin loaded with a withheld capability (reduced function)"
179            );
180        }
181        let mut store = self.new_store(wasi, outcome.grant, budget, Some(&manifest.id))?;
182        let bindings = DecorationsPlugin::instantiate_async(&mut store, component, &self.linker)
183            .await
184            .map_err(|e| PluginHostError::Instantiate(e.into()))?;
185        let id = self.alloc_id();
186        // PO.5: route this plugin's `logging` calls into the tracer (Layer 2).
187        store.data_mut().log_ctx = self.log_ctx_for(id);
188        let (tx, rx) = mpsc::unbounded();
189        let client = DecorationClient { tx, id };
190        let actor = DecorationActor {
191            store,
192            bindings,
193            budget,
194            rx,
195            id,
196            quarantine: crate::Quarantine::new(id, Arc::clone(bus)),
197            tracer: None,
198        };
199        Ok((client, actor))
200    }
201}