cordis-core 0.0.4

A typed, scope-based plugin runtime inspired by Cordis
Documentation
//! Typed isolation and lifecycle events. `Context::isolate::<K>()` opens a
//! fresh slot for `K` while inheriting every other service; `App::start()`
//! emits `Ready`, and every activation emits `Fork` (started) and `Dispose`
//! (torn down). `PluginHandle` also exposes status subscriptions and
//! diagnostics.
//!
//! Run with: `cargo run --example isolation_lifecycle`

use std::sync::Arc;

use cordis_core::{App, Dispose, Fork, Plugin, PluginContext, Ready, Result, ServiceKey};
use parking_lot::Mutex;

struct DbA;
impl ServiceKey for DbA {
    type Value = &'static str;
    const NAME: &'static str = "db-a";
}

struct DbB;
impl ServiceKey for DbB {
    type Value = &'static str;
    const NAME: &'static str = "db-b";
}

/// Watches the built-in lifecycle events. Listener registration is scoped to
/// this activation, so it observes events while it is alive.
struct LifecycleWatcher;

impl Plugin for LifecycleWatcher {
    type Config = ();

    fn name(&self) -> &'static str {
        "lifecycle-watcher"
    }

    async fn apply(&self, ctx: PluginContext, _config: Arc<Self::Config>) -> Result<()> {
        ctx.on::<Ready, _, _>(|_, _| async move {
            println!("lifecycle: Ready");
            Ok(())
        })?;
        ctx.on::<Fork, _, _>(|_, fork| async move {
            println!("lifecycle: Fork({:?}, {:?})", fork.plugin, fork.activation);
            Ok(())
        })?;
        ctx.on::<Dispose, _, _>(|_, dispose| async move {
            println!(
                "lifecycle: Dispose({:?}, {:?})",
                dispose.plugin, dispose.activation
            );
            Ok(())
        })?;
        Ok(())
    }
}

/// Registers `db-a` and `db-b` in the root slot, then registers a different
/// `db-a` into an isolated slot. The parked context lets `main` read both.
struct DualDbPlugin;

impl Plugin for DualDbPlugin {
    type Config = Arc<Mutex<Option<PluginContext>>>;

    fn name(&self) -> &'static str {
        "dual-db"
    }

    async fn apply(&self, ctx: PluginContext, slot: Arc<Self::Config>) -> Result<()> {
        ctx.provide::<DbA>(Arc::new("root-a"))?;
        ctx.provide::<DbB>(Arc::new("root-b"))?;

        // Fresh slot for DbA; DbB is still inherited from the parent.
        let isolated = ctx.isolate::<DbA>();
        isolated.provide::<DbA>(Arc::new("isolated-a"))?;
        *slot.lock() = Some(isolated);
        Ok(())
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let app = App::new();

    let watcher = app.install(LifecycleWatcher, ()).await?;
    watcher.wait_active().await?;

    let slot = Arc::new(Mutex::new(None));
    let dual_db = app.install(DualDbPlugin, slot.clone()).await?;
    dual_db.wait_active().await?;

    // `Ready` is emitted exactly once by the first `start`.
    app.start().await?;

    // Root slot vs isolated slot.
    let root = app.context();
    println!("root.db_a     = {:?}", root.get::<DbA>());
    let isolated = slot
        .lock()
        .as_ref()
        .expect("dual-db has not applied")
        .clone();
    println!("isolated.db_a = {:?}", isolated.get::<DbA>());
    println!("isolated.db_b = {:?}", isolated.get::<DbB>());

    // A manual reload disposes and re-forks the activation.
    println!("--- reloading dual-db ---");
    dual_db.reload().await?;
    println!("watcher status: {:?}", watcher.status());

    // Diagnostics keep a timestamped status history.
    let diagnostics = watcher.diagnostics();
    println!("watcher has {} diagnostics:", diagnostics.len());
    for entry in diagnostics {
        println!("  - {:?}: {:?}", entry.at, entry.status);
    }

    // Shutdown disposes consumers before providers, then the watcher itself.
    println!("--- shutdown ---");
    app.shutdown().await?;
    Ok(())
}