Skip to main content

cordis_core/
context.rs

1use std::{
2    any::TypeId,
3    collections::HashMap,
4    future::Future,
5    marker::PhantomData,
6    ops::Deref,
7    sync::{Arc, Weak},
8};
9
10use futures::FutureExt;
11use tokio_util::sync::CancellationToken;
12
13use crate::{
14    Error, Result, ServiceHandle, ServiceKey,
15    runtime::{EventCallback, QueryCallback, Runtime},
16    scope::ScopeInner,
17    service::{ServiceEntry, ServiceId, boxed_service},
18};
19
20/// Cheaply clonable access to the shared runtime.
21#[derive(Clone)]
22pub struct Context {
23    pub(crate) runtime: Arc<Runtime>,
24    scope_id: u64,
25    isolations: Arc<HashMap<TypeId, u64>>,
26}
27
28impl Context {
29    pub(crate) fn root(runtime: Arc<Runtime>) -> Self {
30        Self {
31            runtime,
32            scope_id: 0,
33            isolations: Arc::new(HashMap::new()),
34        }
35    }
36
37    pub(crate) fn for_scope(runtime: Arc<Runtime>, scope_id: u64) -> Self {
38        Self {
39            runtime,
40            scope_id,
41            isolations: Arc::new(HashMap::new()),
42        }
43    }
44
45    pub fn scope_id(&self) -> u64 {
46        self.scope_id
47    }
48
49    /// Creates a lightweight child context sharing the same runtime.
50    pub fn child(&self) -> Self {
51        Self {
52            runtime: self.runtime.clone(),
53            scope_id: self.runtime.next_id(),
54            isolations: self.isolations.clone(),
55        }
56    }
57
58    /// Creates a child where service `K` resolves in a fresh isolated slot;
59    /// all other service types continue to inherit their current slots.
60    pub fn isolate<K: ServiceKey>(&self) -> Self {
61        let mut isolations = (*self.isolations).clone();
62        isolations.insert(TypeId::of::<K>(), self.runtime.next_id());
63        Self {
64            runtime: self.runtime.clone(),
65            scope_id: self.scope_id,
66            isolations: Arc::new(isolations),
67        }
68    }
69
70    fn service_id<K: ServiceKey>(&self) -> ServiceId {
71        ServiceId {
72            key: TypeId::of::<K>(),
73            isolation: self
74                .isolations
75                .get(&TypeId::of::<K>())
76                .copied()
77                .unwrap_or(0),
78        }
79    }
80
81    pub fn get<K: ServiceKey>(&self) -> Result<Arc<K::Value>> {
82        let services = self.runtime.services.lock().expect("service lock poisoned");
83        let entry = services
84            .get(&self.service_id::<K>())
85            .filter(|entry| entry.active || entry.owner == self.scope_id)
86            .ok_or(Error::MissingService { name: K::NAME })?;
87        entry
88            .value
89            .downcast_ref::<Arc<K::Value>>()
90            .cloned()
91            .ok_or(Error::ServiceTypeMismatch { name: entry.name })
92    }
93
94    pub fn try_get<K: ServiceKey>(&self) -> Option<Arc<K::Value>> {
95        let services = self.runtime.services.lock().expect("service lock poisoned");
96        let entry = services.get(&self.service_id::<K>())?;
97        if !entry.active && entry.owner != self.scope_id {
98            return None;
99        }
100        entry.value.downcast_ref::<Arc<K::Value>>().cloned()
101    }
102
103    /// Invokes listeners serially in registration order.
104    pub async fn emit<E: Event>(&self, event: E) -> Result<()> {
105        self.runtime.emit_serial(event).await
106    }
107
108    /// Invokes all listeners concurrently.
109    pub async fn parallel<E: Event>(&self, event: E) -> Result<()> {
110        self.runtime.emit_parallel(event).await
111    }
112
113    /// Runs query handlers in registration order and returns the first answer.
114    pub async fn query<Q: Query>(&self, query: Q) -> Result<Option<Q::Response>> {
115        self.runtime.query(query).await
116    }
117}
118
119#[derive(Clone, Copy, Debug)]
120pub struct Ready;
121
122#[derive(Clone, Copy, Debug)]
123pub struct Fork {
124    pub plugin: crate::PluginId,
125    pub activation: crate::ActivationId,
126}
127
128#[derive(Clone, Copy, Debug)]
129pub struct Dispose {
130    pub plugin: crate::PluginId,
131    pub activation: crate::ActivationId,
132}
133
134/// Any thread-safe static value can be used as an event payload.
135pub trait Event: Send + Sync + 'static {}
136impl<T: Send + Sync + 'static> Event for T {}
137
138/// A typed bail/query event.
139pub trait Query: Send + Sync + 'static {
140    type Response: Send + Sync + 'static;
141}
142
143/// Context passed to a plugin. Registrations are automatically owned by its
144/// scope and survive when their returned handles are ignored.
145#[derive(Clone)]
146pub struct PluginContext {
147    context: Context,
148    scope: Arc<ScopeInner>,
149}
150
151impl PluginContext {
152    pub(crate) fn new(context: Context, scope: Arc<ScopeInner>) -> Self {
153        Self { context, scope }
154    }
155
156    pub fn isolate<K: ServiceKey>(&self) -> Self {
157        Self {
158            context: self.context.isolate::<K>(),
159            scope: self.scope.clone(),
160        }
161    }
162
163    pub fn provide<K: ServiceKey>(&self, value: Arc<K::Value>) -> Result<ServiceHandle<K>> {
164        let id = self.context.service_id::<K>();
165        let token = self.context.runtime.next_service_generation();
166        let generation = self.context.runtime.next_service_generation();
167        {
168            let mut services = self
169                .context
170                .runtime
171                .services
172                .lock()
173                .expect("service lock poisoned");
174            if services.contains_key(&id) {
175                return Err(Error::DuplicateService { name: K::NAME });
176            }
177            services.insert(
178                id,
179                ServiceEntry {
180                    value: boxed_service::<K>(value),
181                    owner: self.scope.id,
182                    token,
183                    generation,
184                    name: K::NAME,
185                    active: false,
186                },
187            );
188        }
189
190        let weak = Arc::downgrade(&self.context.runtime);
191        let owner = self.scope.id;
192        if let Err(error) = self.scope.push(Box::new(move || {
193            Box::pin(async move {
194                if let Some(runtime) = weak.upgrade() {
195                    runtime.remove_service(id, owner, token);
196                }
197                Ok(())
198            })
199        })) {
200            self.context.runtime.remove_service(id, owner, token);
201            return Err(error);
202        }
203
204        Ok(ServiceHandle {
205            id,
206            runtime: Arc::downgrade(&self.context.runtime),
207            owner,
208            token,
209            _key: PhantomData,
210        })
211    }
212
213    pub fn on<E, H, Fut>(&self, handler: H) -> Result<ListenerHandle>
214    where
215        E: Event,
216        H: Fn(Context, Arc<E>) -> Fut + Send + Sync + 'static,
217        Fut: Future<Output = Result<()>> + Send + 'static,
218    {
219        let event_key = TypeId::of::<E>();
220        let context = self.context.clone();
221        let callback: Arc<EventCallback> = Arc::new(move |event| {
222            let result = event.downcast::<E>();
223            let context = context.clone();
224            match result {
225                Ok(event) => handler(context, event).boxed(),
226                Err(_) => {
227                    async { Err(Error::Cleanup("event payload type mismatch".into())) }.boxed()
228                }
229            }
230        });
231        let id = self
232            .context
233            .runtime
234            .add_listener(event_key, self.scope.id, callback);
235        let weak = Arc::downgrade(&self.context.runtime);
236        if let Err(error) = self.scope.push(Box::new(move || {
237            Box::pin(async move {
238                if let Some(runtime) = weak.upgrade() {
239                    runtime.remove_listener(event_key, id);
240                }
241                Ok(())
242            })
243        })) {
244            self.context.runtime.remove_listener(event_key, id);
245            return Err(error);
246        }
247        Ok(ListenerHandle {
248            runtime: Arc::downgrade(&self.context.runtime),
249            key: event_key,
250            id,
251            query: false,
252        })
253    }
254
255    pub fn on_query<Q, H, Fut>(&self, handler: H) -> Result<ListenerHandle>
256    where
257        Q: Query,
258        H: Fn(Context, Arc<Q>) -> Fut + Send + Sync + 'static,
259        Fut: Future<Output = Result<Option<Q::Response>>> + Send + 'static,
260    {
261        let key = TypeId::of::<Q>();
262        let context = self.context.clone();
263        let callback: Arc<QueryCallback> = Arc::new(move |query| {
264            let result = query.downcast::<Q>();
265            let context = context.clone();
266            match result {
267                Ok(query) => handler(context, query)
268                    .map(|result| {
269                        result.map(|response| {
270                            response.map(|value| {
271                                Box::new(value) as Box<dyn std::any::Any + Send + Sync>
272                            })
273                        })
274                    })
275                    .boxed(),
276                Err(_) => {
277                    async { Err(Error::Cleanup("query payload type mismatch".into())) }.boxed()
278                }
279            }
280        });
281        let id = self
282            .context
283            .runtime
284            .add_query_listener(key, self.scope.id, callback);
285        let weak = Arc::downgrade(&self.context.runtime);
286        if let Err(error) = self.scope.push(Box::new(move || {
287            Box::pin(async move {
288                if let Some(runtime) = weak.upgrade() {
289                    runtime.remove_query_listener(key, id);
290                }
291                Ok(())
292            })
293        })) {
294            self.context.runtime.remove_query_listener(key, id);
295            return Err(error);
296        }
297        Ok(ListenerHandle {
298            runtime: Arc::downgrade(&self.context.runtime),
299            key,
300            id,
301            query: true,
302        })
303    }
304
305    pub fn defer<F, Fut>(&self, cleanup: F) -> Result<()>
306    where
307        F: FnOnce() -> Fut + Send + 'static,
308        Fut: Future<Output = Result<()>> + Send + 'static,
309    {
310        self.scope.push(Box::new(move || Box::pin(cleanup())))
311    }
312
313    pub fn manage<R: Resource>(&self, resource: R) -> Result<()> {
314        let resource = Arc::new(std::sync::Mutex::new(Some(resource)));
315        let start_resource = resource.clone();
316        self.scope.on_commit(Box::new(move || {
317            Box::pin(async move {
318                let resource = start_resource
319                    .lock()
320                    .expect("resource lock poisoned")
321                    .take()
322                    .ok_or_else(|| Error::cleanup("resource already consumed"))?;
323                let result = resource.start().await;
324                *start_resource.lock().expect("resource lock poisoned") = Some(resource);
325                result
326            })
327        }))?;
328
329        self.scope.push(Box::new(move || {
330            Box::pin(async move {
331                let resource = resource.lock().expect("resource lock poisoned").take();
332                if let Some(resource) = resource {
333                    resource.cancel();
334                    Box::new(resource).dispose().await?;
335                }
336                Ok(())
337            })
338        }))
339    }
340
341    /// Registers a cooperative task. It is spawned only when the activation
342    /// commits. Disposal waits five seconds, then aborts an unresponsive task.
343    pub fn spawn<F, Fut>(&self, task: F) -> Result<TaskHandle>
344    where
345        F: FnOnce(CancellationToken) -> Fut + Send + 'static,
346        Fut: Future<Output = Result<()>> + Send + 'static,
347    {
348        let token = CancellationToken::new();
349        let join = Arc::new(std::sync::Mutex::new(None));
350        let start_join = join.clone();
351        let child = token.child_token();
352        self.scope.on_commit(Box::new(move || {
353            Box::pin(async move {
354                let future = std::panic::AssertUnwindSafe(task(child))
355                    .catch_unwind()
356                    .map(|result| match result {
357                        Ok(result) => result,
358                        Err(payload) => Err(Error::panic(payload)),
359                    });
360                *start_join.lock().expect("task lock poisoned") = Some(tokio::spawn(future));
361                Ok(())
362            })
363        }))?;
364
365        let cleanup_token = token.clone();
366        self.scope.push(Box::new(move || {
367            Box::pin(async move {
368                cleanup_token.cancel();
369                let join = join.lock().expect("task lock poisoned").take();
370                if let Some(mut join) = join {
371                    match tokio::time::timeout(std::time::Duration::from_secs(5), &mut join).await {
372                        Ok(result) => result??,
373                        Err(_) => {
374                            join.abort();
375                            let _ = join.await;
376                            return Err(Error::TaskTimeout { seconds: 5 });
377                        }
378                    }
379                }
380                Ok(())
381            })
382        }))?;
383        Ok(TaskHandle { token })
384    }
385}
386
387impl Deref for PluginContext {
388    type Target = Context;
389    fn deref(&self) -> &Self::Target {
390        &self.context
391    }
392}
393
394/// Non-owning early-cancellation handle. `Drop` intentionally does nothing.
395pub struct ListenerHandle {
396    runtime: Weak<Runtime>,
397    key: TypeId,
398    id: u64,
399    query: bool,
400}
401
402impl ListenerHandle {
403    pub fn cancel(&self) -> bool {
404        self.runtime.upgrade().is_some_and(|runtime| {
405            if self.query {
406                runtime.remove_query_listener(self.key, self.id)
407            } else {
408                runtime.remove_listener(self.key, self.id)
409            }
410        })
411    }
412}
413
414pub struct TaskHandle {
415    token: CancellationToken,
416}
417impl TaskHandle {
418    pub fn cancel(&self) {
419        self.token.cancel();
420    }
421}
422
423pub trait Resource: Send + Sync + 'static {
424    fn start(&self) -> impl Future<Output = Result<()>> + Send {
425        async { Ok(()) }
426    }
427
428    fn cancel(&self) {}
429
430    fn dispose(self: Box<Self>) -> impl Future<Output = Result<()>> + Send;
431}