Skip to content

Services & Plugins

Overview

Two systems provide extensibility at different levels:

Service = long-lived runtime capability (starts with app, stays running)
Plugin  = extension package (contributes commands, tasks, resources, services, handlers)

They are complementary:

flowchart TD
    P["Plugin"] -->|"may provide"| S["Service"]
    P -->|"may provide"| C["Command"]
    P -->|"may provide"| T["Task"]
    P -->|"may provide"| R["Resource"]
    P -->|"may provide"| EH["EventHandler"]
    P -->|"may provide"| SS["Signal subscription"]

A plugin is the package. A service is one capability inside it.


Part 1: Services

What Is a Service?

Task    = finite work instance (has a start and end)
Service = long-lived capability (starts with app, stays alive, stops with app)
Task Service
Lifetime Finite — Pending → Completed Long — starts with app, stays running
Created by Command or direct call Registration before engine.start()
Lifecycle Task lifecycle (6 states) Service lifecycle (5 states)
Example Export project Autosave service

The Service Trait

use app_shell::app_engine::{Service, ServiceError, EngineRef};

struct AutosaveService {
    active: bool,
}

impl Service for AutosaveService {
    fn service_type(&self) -> &str { "autosave" }

    fn initialize(&mut self, _engine: &EngineRef) -> Result<(), ServiceError> {
        println!("[Autosave] Initializing");
        Ok(())
    }

    fn start(&mut self, _engine: &EngineRef) -> Result<(), ServiceError> {
        self.active = true;
        println!("[Autosave] Started — monitoring for changes");
        Ok(())
    }

    fn stop(&mut self) -> Result<(), ServiceError> {
        self.active = false;
        println!("[Autosave] Stopped");
        Ok(())
    }

    fn dispose(&mut self) -> Result<(), ServiceError> {
        println!("[Autosave] Disposed");
        Ok(())
    }
}

Service Registration

use app_shell::app_engine::ServiceDefinition;

let service_id = engine.service_manager_mut().register(
    ServiceDefinition::new("autosave", "Autosave", "Automatic document saving")
        .with_metadata("interval", "300"),
    Box::new(AutosaveService { active: false }),
)?;

assert_eq!(engine.service_manager().count(), 1);
assert!(engine.service_manager().contains_type("autosave"));
assert_eq!(
    engine.service_manager().state(service_id),
    Some(ServiceState::Registered),
);

Service Lifecycle

Registered → Initialized → Running → Stopped → Disposed
// Individual lifecycle (panic-isolated)
engine.service_manager_mut().initialize(service_id, &engine_ref)?;
assert_eq!(engine.service_manager().state(service_id), Some(ServiceState::Initialized));

engine.service_manager_mut().start(service_id, &engine_ref)?;
assert_eq!(engine.service_manager().state(service_id), Some(ServiceState::Running));

engine.service_manager_mut().stop(service_id)?;
assert_eq!(engine.service_manager().state(service_id), Some(ServiceState::Stopped));

engine.service_manager_mut().dispose(service_id)?;
assert_eq!(engine.service_manager().state(service_id), Some(ServiceState::Disposed));

Automatic Lifecycle (Via Engine)

Services participate automatically in the engine's lifecycle:

// Register services BEFORE engine.initialize()
engine.service_manager_mut().register(
    ServiceDefinition::new("autosave", "Autosave", "Autosave"),
    Box::new(AutosaveService { active: false }),
)?;
engine.service_manager_mut().register(
    ServiceDefinition::new("worker", "Background Worker", "Background processing"),
    Box::new(BackgroundWorkerService { active: false }),
)?;

// Engine lifecycle manages services automatically
engine.initialize()?;  // → initialize_all() (services → Initialized)
engine.start()?;       // → start_all() (services → Running)

// ... application runs — services are active ...

engine.stop()?;        // → stop_all() (services → Stopped, reverse order)
engine.dispose()?;     // → dispose_all() (services → Disposed, reverse order)

Panic Isolation

If a service panics during lifecycle, it doesn't crash the engine:

struct PanickingService;
impl Service for PanickingService {
    fn service_type(&self) -> &str { "panic_svc" }
    fn initialize(&mut self, _engine: &EngineRef) -> Result<(), ServiceError> {
        panic!("service init crashed!");
    }
    fn start(&mut self, _engine: &EngineRef) -> Result<(), ServiceError> { Ok(()) }
    fn stop(&mut self) -> Result<(), ServiceError> { Ok(()) }
    fn dispose(&mut self) -> Result<(), ServiceError> { Ok(()) }
}

let id = engine.service_manager_mut().register(
    ServiceDefinition::new("panic_svc", "Panic", "Panics"),
    Box::new(PanickingService),
)?;

let result = engine.service_manager_mut().initialize(id, &engine_ref);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("service panicked"));
// Engine continues running — no crash

Service State Validation

// Can't start before initialize
let result = engine.service_manager_mut().start(id, &engine_ref);
assert!(matches!(result, Err(ServiceError::InvalidState { .. })));

engine.service_manager_mut().initialize(id, &engine_ref)?;

// Can't initialize twice
let result = engine.service_manager_mut().initialize(id, &engine_ref);
assert!(matches!(result, Err(ServiceError::InvalidState { .. })));

engine.service_manager_mut().start(id, &engine_ref)?;

// Can't dispose while running (must stop first)
let result = engine.service_manager_mut().dispose(id);
assert!(matches!(result, Err(ServiceError::InvalidState { .. })));

Service: Autosave Example

A real autosave service that reacts to events and schedules tasks:

struct AutosaveService {
    save_count: usize,
}

impl Autoservice {
    fn new() -> Self {
        Self { save_count: 0 }
    }
}

impl Service for AutosaveService {
    fn service_type(&self) -> &str { "autosave" }

    fn initialize(&mut self, engine: &EngineRef) -> Result<(), ServiceError> {
        // Subscribe to document change events
        // (Note: subscribe requires &mut EventBus — must be done
        //  before the engine starts, not during initialize())
        Ok(())
    }

    fn start(&mut self, engine: &EngineRef) -> Result<(), ServiceError> {
        // In a real service, this would start a periodic check
        // The service could use the scheduler to submit backup tasks
        // engine.scheduler().submit(backup_task_id, Schedule::Interval(Duration::from_secs(300)), 3);
        self.save_count = 0;
        Ok(())
    }

    fn stop(&mut self) -> Result<(), ServiceError> {
        // Perform a final save
        Ok(())
    }

    fn dispose(&mut self) -> Result<(), ServiceError> {
        Ok(())
    }
}

The service doesn't perform the save itself. It coordinates: detects when a save should happen and creates/schedules a task to do it.

Service vs Task

flowchart TD
    A["AutosaveService (long-lived)"] -->|"detects document change (via event)"| B["Command"]
    B --> C["Task (finite — one save operation)"]
    C --> D["TaskExecutor → domain save function"]
    D --> E["TaskResult"]

The service doesn't implement saving. It coordinates: detects when a save should happen and creates/schedules a task.

Service Dependencies

Services can depend on other App Engine systems:

flowchart TD
    A["BackgroundWorkerService"] -->|"uses"| B["Scheduler (to dispatch work)"]
    A -->|"uses"| C["ResourceManager (to acquire data)"]
    A -->|"uses"| D["EventBus (to publish results)"]

Prefer services that depend on App Engine capabilities rather than services depending on other services:

✓ Service → Scheduler / EventBus / ResourceManager
✗ Service A → Service B → Service C → Service D  (chain)

If shared behavior is needed, it should live in an App Engine subsystem, not be chained through services.


Part 2: Plugins

What Is a Plugin?

Service = one long-lived capability
Plugin  = an extension package that may provide multiple capabilities

A plugin can contribute:

flowchart TD
    P["ImagePlugin"] --> C["Commands (ImportImage, ExportImage)"]
    P --> T["Tasks (RenderImage)"]
    P --> R["Resources (ImageResource, ImageLoader)"]
    P --> S["Services (ImageCacheService)"]
    P --> E["Event handlers (subscribe to DocumentChanged)"]

The Plugin Trait

use app_shell::app_engine::{
    Plugin, PluginContext, PluginError, EngineRef,
};

struct ImagePlugin {
    initialized: bool,
}

impl Plugin for ImagePlugin {
    fn id(&self) -> &str { "image-plugin" }

    fn initialize(&mut self, ctx: &PluginContext, _engine: &EngineRef) -> Result<(), PluginError> {
        println!("[ImagePlugin] Initializing (context: {})", ctx.plugin_id());
        self.initialized = true;
        Ok(())
    }

    fn start(&mut self, _engine: &EngineRef) -> Result<(), PluginError> {
        println!("[ImagePlugin] Started");
        Ok(())
    }

    fn stop(&mut self) -> Result<(), PluginError> {
        println!("[ImagePlugin] Stopped");
        Ok(())
    }

    fn dispose(&mut self) -> Result<(), PluginError> {
        println!("[ImagePlugin] Disposed");
        Ok(())
    }
}

Plugin Manifest

A manifest describes a plugin without loading its implementation:

use app_shell::app_engine::PluginManifest;

let manifest = PluginManifest::new("image-plugin", "Image Plugin")
    .with_version("2.0.0")
    .with_description("Image import, export, and processing")
    .with_author("Alice")
    .with_dependency("core-plugin")   // requires another plugin
    .with_capability("commands")      // provides commands
    .with_capability("resources")     // provides resource loaders
    .with_capability("services")      // provides services
    .with_permissions(PluginPermissions::new( 
        LOAD_RESOURCES | REGISTER_COMMANDS | SUBSCRIBE_EVENTS
    ))
    .with_engine_api_version(Version::new(1, 0, 0));

Plugin Registration

let plugin_id = engine.plugin_manager_mut().register(
    PluginManifest::new("image-plugin", "Image Plugin"),
    Box::new(ImagePlugin { initialized: false }),
)?;

assert_eq!(engine.plugin_manager().count(), 1);
assert!(engine.plugin_manager().contains_type("image-plugin"));
assert_eq!(
    engine.plugin_manager().state(plugin_id),
    Some(PluginState::Loaded),
);

Plugin Lifecycle

Discovered → Loaded → Initialized → Started → Stopped → Unloaded
// Individual lifecycle (panic-isolated)
engine.plugin_manager_mut().initialize(plugin_id, &engine_ref)?;
assert_eq!(engine.plugin_manager().state(plugin_id), Some(PluginState::Initialized));

engine.plugin_manager_mut().start(plugin_id, &engine_ref)?;
assert_eq!(engine.plugin_manager().state(plugin_id), Some(PluginState::Started));

engine.plugin_manager_mut().stop(plugin_id)?;
assert_eq!(engine.plugin_manager().state(plugin_id), Some(PluginState::Stopped));

engine.plugin_manager_mut().dispose(plugin_id)?;
assert_eq!(engine.plugin_manager().state(plugin_id), Some(PluginState::Unloaded));

Automatic Lifecycle (Via Engine)

Plugins participate in the engine's lifecycle, after services:

// Register plugins BEFORE engine.initialize()
engine.plugin_manager_mut().register(
    PluginManifest::new("image-plugin", "Image Plugin"),
    Box::new(ImagePlugin::new()),
)?;

// Engine lifecycle manages plugins automatically
engine.initialize()?;  // → init services, then init plugins
engine.start()?;       // → start services, then start plugins

// ... application runs — plugins are active ...

engine.stop()?;        // → stop plugins first, then stop services
engine.dispose()?;     // → dispose plugins first, then dispose services

Panic Isolation

If a plugin panics during lifecycle, it doesn't crash the engine:

struct PanickingPlugin;
impl Plugin for PanickingPlugin {
    fn id(&self) -> &str { "panic_plugin" }
    fn initialize(&mut self, _: &PluginContext, _: &EngineRef) -> Result<(), PluginError> {
        panic!("plugin init crashed!");
    }
    fn start(&mut self, _: &EngineRef) -> Result<(), PluginError> { Ok(()) }
    fn stop(&mut self) -> Result<(), PluginError> { Ok(()) }
    fn dispose(&mut self) -> Result<(), PluginError> { Ok(()) }
}

let id = engine.plugin_manager_mut().register(
    PluginManifest::new("panic_plugin", "Panic Plugin"),
    Box::new(PanickingPlugin),
)?;

let result = engine.plugin_manager_mut().initialize(id, &engine_ref);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("plugin panicked"));
// Engine continues running — no crash

PluginContext

The PluginContext is created by the PluginManager during initialization:

impl Plugin for MyPlugin {
    fn initialize(&mut self, ctx: &PluginContext, engine: &EngineRef) -> Result<(), PluginError> {
        // The context provides the plugin's identity
        assert_eq!(ctx.plugin_id(), "my_plugin");

        // The engine provides access to all subsystems
        // engine.event_bus, engine.resource_manager, etc.

        Ok(())
    }
}

PluginContext is intentionally minimal — it provides the plugin's identity and engine metadata, not unlimited access to the runtime.

Plugin Dependencies

A plugin may depend on another plugin:

// Plugin "a" depends on plugin "b"
let manifest = PluginManifest::new("a", "A")
    .with_dependency("b");

engine.plugin_manager_mut().register(manifest, Box::new(PluginA))?;

// "b" is not registered → validation fails
assert!(engine.plugin_manager_mut().validate_all_dependencies().is_err());

// Register "b"
engine.plugin_manager_mut().register(
    PluginManifest::new("b", "B"),
    Box::new(PluginB),
)?;

// Now validation passes
engine.plugin_manager_mut().validate_all_dependencies()?;

initialize_all() validates dependencies before initializing:

// If dependencies are not met, initialize_all() fails:
engine.plugin_manager_mut().initialize_all(&engine_ref)
    .expect_err("dependency not met");

Part 3: Plugin Permissions

Permission Flags

use app_shell::app_engine::PluginPermissions;

// No permissions
let none = PluginPermissions::new(0);

// Full access (default — backward compatible)
let full = PluginPermissions::FULL;

// Combined permissions
let limited = PluginPermissions::new(
    REGISTER_COMMANDS | SUBSCRIBE_EVENTS,
);
assert!(limited.can_register_commands());
assert!(limited.can_subscribe_events());
assert!(!limited.can_load_resources());
assert!(!limited.can_write_config());

Manifest with Permissions

let manifest = PluginManifest::new("limited", "Limited Plugin")
    .with_permissions(PluginPermissions::new(
        REGISTER_COMMANDS | SUBSCRIBE_EVENTS,
    ));
// Can register commands and subscribe to events
// Cannot load resources or write config

Permission Queries on PluginManager

let id = engine.plugin_manager_mut().register(
    PluginManifest::new("limited", "Limited")
        .with_permissions(PluginPermissions::new(REGISTER_COMMANDS)),
    Box::new(StubPlugin),
)?;

assert!(engine.plugin_manager().can_register_commands(id));
assert!(!engine.plugin_manager().can_load_resources(id));
assert!(!engine.plugin_manager().can_publish_events(id));

PermissionCheckedEngineRef

For explicit permission enforcement, wrap EngineRef with permission checks:

use app_shell::app_engine::PermissionCheckedEngineRef;

// In a plugin's initialize():
fn initialize(&mut self, _ctx: &PluginContext, engine: &EngineRef) -> Result<(), PluginError> {
    // Wrap engine with permission checking
    let checked = PermissionCheckedEngineRef::new(
        engine,
        PluginPermissions::new(LOAD_RESOURCES | SUBSCRIBE_EVENTS),
        "my_plugin",
    );

    // Read-only access — always allowed
    let config = checked.config_store();
    let prefs = checked.preferences();

    // Permission-checked access
    let mgr = checked.try_load_resource()?;   // OK — has LOAD_RESOURCES
    let result = checked.try_write_config();  // Err — no WRITE_CONFIG
    assert!(matches!(result, Err(PluginSecurityError::PermissionDenied { .. })));

    Ok(())
}

try_* Methods

Method Permission Required Returns
try_write_config() WRITE_CONFIG Result<&ConfigStore, PluginSecurityError>
try_load_resource() LOAD_RESOURCES Result<&ResourceManager, PluginSecurityError>
try_publish_event() PUBLISH_EVENTS Result<&EventBus, PluginSecurityError>
try_subscribe_events() SUBSCRIBE_EVENTS Result<&EventBus, PluginSecurityError>
try_register_commands() REGISTER_COMMANDS Result<&CommandExecutor, PluginSecurityError>
try_register_tasks() REGISTER_TASKS Result<&CommandExecutor, PluginSecurityError>
try_register_services() REGISTER_SERVICES Result<&CommandExecutor, PluginSecurityError>

Read-only access (config read, state read, history read, etc.) is always allowed regardless of permissions.

Full Access (Backward Compatible)

// Default permissions = FULL_ACCESS
let checked = PermissionCheckedEngineRef::new(
    engine,
    PluginPermissions::default(),  // = FULL
    "full_access_plugin",
);

// All try_* methods succeed
assert!(checked.try_load_resource().is_ok());
assert!(checked.try_write_config().is_ok());
assert!(checked.try_publish_event().is_ok());

Restricted Plugin Still Gets Lifecycle

// A plugin with NO permissions still has a normal lifecycle:
let id = engine.plugin_manager_mut().register(
    PluginManifest::new("restricted", "Restricted")
        .with_permissions(PluginPermissions::new(0)),
    Box::new(StubPlugin),
)?;

// Lifecycle works — permissions don't block lifecycle
engine.plugin_manager_mut().initialize(id, &engine_ref)?;
engine.plugin_manager_mut().start(id, &engine_ref)?;
engine.plugin_manager_mut().stop(id)?;
engine.plugin_manager_mut().dispose(id)?;

Part 4: Dynamic Plugin Loading

DynamicPluginLoader

The DynamicPluginLoader is a contract stub for loading plugins from shared libraries (.so/.dll/.dylib):

use app_shell::app_engine::DynamicPluginLoader;

let loader = DynamicPluginLoader::new();

// Detect shared libraries
assert!(DynamicPluginLoader::is_shared_library("plugin.so"));
assert!(DynamicPluginLoader::is_shared_library("plugin.dll"));
assert!(DynamicPluginLoader::is_shared_library("plugin.dylib"));
assert!(!DynamicPluginLoader::is_shared_library("plugin.rs"));

With search paths:

let mut loader = DynamicPluginLoader::new();
loader.add_search_path("/tmp/plugins");

// Load from a file path
// (Actual loading requires the `libloading` crate — this is a stub)
let result = loader.load("my_plugin.so");
assert!(matches!(result, Err(PluginError::LoadFailed { .. })));

The contract for dynamic plugins: 1. The shared library exports create_plugin() -> Box<dyn Plugin> 2. The shared library exports create_manifest() -> PluginManifest 3. The loader calls both, verifies the manifest, and returns them


Plugin → App Engine Integration

A plugin contributes to the engine through established contracts. Here's a complete example:

use app_shell::app_engine::*;
use std::any::Any;
use std::sync::{Arc, Mutex};

// --- Plugin-provided command handler ---

struct ImportImageHandler;
impl CommandHandler for ImportImageHandler {
    fn execute(&self, input: &CommandInput, _ctx: &CommandContext, _engine: &EngineRef) -> Result<CommandResult, CommandError> {
        let path: &String = input.get::<String>()
            .ok_or_else(|| CommandError::InvalidArguments("expected path".to_string()))?;
        Ok(CommandResult::success_with(format!("imported:{}", path)))
    }
}

// --- Plugin-provided resource loader ---

struct ImageResourceLoader;
impl ResourceLoader for ImageResourceLoader {
    fn resource_type(&self) -> &str { "image" }
    fn load(&self, source: &str) -> Result<Box<dyn Any + Send>, ResourceError> {
        Ok(Box::new(format!("pixels:{}", source)))
    }
}

// --- Plugin-provided service ---

struct ImageCacheService;
impl Service for ImageCacheService {
    fn service_type(&self) -> &str { "image_cache" }
    fn initialize(&mut self, _engine: &EngineRef) -> Result<(), ServiceError> { Ok(()) }
    fn start(&mut self, _engine: &EngineRef) -> Result<(), ServiceError> { Ok(()) }
    fn stop(&mut self) -> Result<(), ServiceError> { Ok(()) }
    fn dispose(&mut self) -> Result<(), ServiceError> { Ok(()) }
}

// --- Plugin-provided task handler ---

struct RenderImageTask;
impl TaskHandler for RenderImageTask {
    fn execute(&self, input: &TaskInput, _ctx: &TaskContext, _engine: &EngineRef) -> Result<TaskResult, TaskError> {
        let path: &String = input.get::<String>()
            .ok_or_else(|| TaskError::ExecutionFailed {
                id: 0, reason: "expected path".into()
            })?;
        Ok(TaskResult::completed_with(format!("rendered:{}", path)))
    }
}

// --- The plugin itself ---

struct ImagePlugin;

impl Plugin for ImagePlugin {
    fn id(&self) -> &str { "image-plugin" }

    fn initialize(&mut self, _ctx: &PluginContext, _engine: &EngineRef) -> Result<(), PluginError> {
        // In a real plugin, start() would register commands, resources, etc.
        // The engine reference would be used to access CommandExecutor, ResourceManager, etc.
        Ok(())
    }

    fn start(&mut self, _engine: &EngineRef) -> Result<(), PluginError> { Ok(()) }
    fn stop(&mut self) -> Result<(), PluginError> { Ok(()) }
    fn dispose(&mut self) -> Result<(), PluginError> { Ok(()) }
}

// --- Registration function ---

pub fn register_with_engine(engine: &mut AppEngine) -> Result<(), Box<dyn std::error::Error>> {
    // Register plugin
    let pid = engine.plugin_manager_mut().register(
        PluginManifest::new("image-plugin", "Image Plugin")
            .with_permissions(PluginPermissions::FULL)
            .with_capability("commands")
            .with_capability("resources")
            .with_capability("services")
            .with_capability("tasks"),
        Box::new(ImagePlugin),
    )?;

    // Initialize and start the plugin
    // (In a real app, engine.initialize() and engine.start() handle this)
    engine.plugin_manager_mut().initialize(pid, &engine.engine_ref())?;
    engine.plugin_manager_mut().start(pid, &engine.engine_ref())?;

    // Register the plugin's contributions with the engine
    engine.command_executor_mut().register(
        CommandDefinition::new("image.import", "Import Image", "Import an image file"),
        Box::new(ImportImageHandler),
    )?;

    engine.resource_manager().register_loader(Box::new(ImageResourceLoader));

    engine.service_manager_mut().register(
        ServiceDefinition::new("image_cache", "Image Cache", "Image cache service"),
        Box::new(ImageCacheService),
    )?;

    engine.task_manager_mut().register(
        TaskDefinition::new("image.render", "Render Image", "Render an image"),
        Box::new(RenderImageTask),
    )?;

    Ok(())
}

The App Engine never knew that ImportImageHandler came from a plugin. It just saw a CommandHandler registered with the CommandExecutor.


When to Create a Service vs a Plugin vs a New Subsystem

When to Use a Service

✓ Long-lived capability that starts/stops with the app
✓ Coordinates other systems (scheduler, events, resources)
✓ Does NOT contain the core domain logic

Example: AutosaveService — watches for changes, schedules save tasks. Doesn't implement saving itself.

When to Use a Plugin

✓ Extension package that bundles multiple capabilities
✓ Can be added/removed independently
✓ May provide commands, tasks, resources, services, and event handlers

Example: ImagePlugin — bundles image import/export commands, image resource loader, image cache service, and render task.

When to Create a New App Engine Subsystem

✓ The behavior is fundamental to all applications
✓ Every domain needs it, not just one
✓ It has its own complex internal architecture

If something starts as a service and grows to have its own managers, registries, loaders, and lifecycle — it may need to become a subsystem.

Plugin → Service → Subsystem (growth path)

Example:

flowchart LR
    A["ImageCacheService (simple)"] -->|"grows into"| B["ResourceManager<br/>(full subsystem with loaders, caches, handles)"]
Scale Solution
One capability, short code Service
Bundle of related capabilities Plugin
Complex internal architecture used by all apps App Engine subsystem

Lifecycle Ordering Summary

Startup (forward)

flowchart TD
    A["1. Runtime: Created → Initialized"] --> B["2. Services: Registered → Initialized"]
    B --> C["3. Plugins: Loaded → Initialized"]
    C --> D["4. Runtime: Initialized → Running"]
    D --> E["5. Services: Initialized → Running"]
    E --> F["6. Plugins: Initialized → Started"]

Shutdown (reverse)

flowchart TD
    A["1. Runtime: stop_requested = true"] --> B["2. TaskManager: cancel_all()<br/>(cancel running tasks)"]
    B --> C["3. Plugins: Started → Stopped"]
    C --> D["4. Services: Running → Stopped"]
    D --> E["5. Runtime: Running → Stopped"]
    E --> F["6. Plugins: Stopped → Unloaded"]
    F --> G["7. Services: Stopped → Disposed"]
    G --> H["8. ResourceManager: unload_all()<br/>(unload non-in-use resources)"]
    H --> I["9. Runtime: Stopped → Disposed"]

Quick Reference

Service API

// Register
let id = engine.service_manager_mut().register(definition, service)?;

// Lifecycle (panic-isolated)
engine.service_manager_mut().initialize(id, &engine_ref)?;
engine.service_manager_mut().start(id, &engine_ref)?;
engine.service_manager_mut().stop(id)?;
engine.service_manager_mut().dispose(id)?;

// Bulk (called automatically by engine lifecycle)
engine.service_manager_mut().initialize_all(&engine_ref)?;
engine.service_manager_mut().start_all(&engine_ref)?;
engine.service_manager_mut().stop_all()?;
engine.service_manager_mut().dispose_all()?;

// Query
engine.service_manager().get(id);                 // Option<&dyn Service>
engine.service_manager().state(id);               // Option<ServiceState>
engine.service_manager().contains_type("type");   // bool
engine.service_manager().get_definition(id);      // Option<&ServiceDefinition>
engine.service_manager().count();                 // usize

Plugin API

// Register
let id = engine.plugin_manager_mut().register(manifest, plugin)?;

// Loader
engine.plugin_manager_mut().register_loader(loader);
let id = engine.plugin_manager_mut().load("source")?;

// Lifecycle (panic-isolated)
engine.plugin_manager_mut().initialize(id, &engine_ref)?;
engine.plugin_manager_mut().start(id, &engine_ref)?;
engine.plugin_manager_mut().stop(id)?;
engine.plugin_manager_mut().dispose(id)?;

// Bulk (called automatically by engine lifecycle)
engine.plugin_manager_mut().initialize_all(&engine_ref)?;
engine.plugin_manager_mut().start_all(&engine_ref)?;
engine.plugin_manager_mut().stop_all()?;
engine.plugin_manager_mut().dispose_all()?;

// Dependencies
engine.plugin_manager_mut().validate_dependencies(id)?;
engine.plugin_manager_mut().validate_all_dependencies()?;

// Permissions
engine.plugin_manager().permissions(id);            // Option<PluginPermissions>
engine.plugin_manager().can_register_commands(id);  // bool
engine.plugin_manager().can_load_resources(id);     // bool
engine.plugin_manager().can_publish_events(id);     // bool

// Query
engine.plugin_manager().get(id);             // Option<&dyn Plugin>
engine.plugin_manager().state(id);           // Option<PluginState>
engine.plugin_manager().contains_type("id"); // bool
engine.plugin_manager().get_manifest(id);    // Option<&PluginManifest>
engine.plugin_manager().count();             // usize

PermissionCheckedEngineRef API

let checked = PermissionCheckedEngineRef::new(engine, permissions, "plugin_id");

// Read-only (always allowed)
checked.config_store();
checked.preferences();
checked.state_manager();
checked.history_store();

// Permission-checked
checked.try_write_config()?;       // requires WRITE_CONFIG
checked.try_load_resource()?;      // requires LOAD_RESOURCES
checked.try_publish_event()?;      // requires PUBLISH_EVENTS
checked.try_subscribe_events()?;   // requires SUBSCRIBE_EVENTS
checked.try_register_commands()?;  // requires REGISTER_COMMANDS

Next Steps