alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
use super::ValidatedPluginRegistry;
use std::num::NonZeroUsize;

/// Maximum queued sealed batches across enabled plugins.
pub const MAX_PLUGIN_SCHEDULER_INTENT_BATCHES: usize = 1_024;
/// Minimum queue cap when no enabled plugin contributes authority.
const MIN_PLUGIN_SCHEDULER_INTENT_BATCHES: NonZeroUsize = NonZeroUsize::MIN;

/// Registry-level guest publication policy.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PluginSchedulerPolicy {
    /// Sealed batches accepted by the ECS adapter.
    max_intent_batches: NonZeroUsize,
}

impl PluginSchedulerPolicy {
    /// Sums enabled plugin burst caps under the host ceiling.
    #[must_use]
    pub fn from_validated_registry(registry: &ValidatedPluginRegistry) -> Self {
        let max_intent_batches = registry
            .enabled_plugins()
            .map(|plugin| plugin.runtime_limits().max_intent_batches())
            .try_fold(0_usize, usize::checked_add)
            .unwrap_or(usize::MAX)
            .min(MAX_PLUGIN_SCHEDULER_INTENT_BATCHES);

        let max_intent_batches = max_intent_batches.max(MIN_PLUGIN_SCHEDULER_INTENT_BATCHES.get());
        Self {
            max_intent_batches: NonZeroUsize::new(max_intent_batches)
                .unwrap_or(MIN_PLUGIN_SCHEDULER_INTENT_BATCHES),
        }
    }

    /// Returns the sealed-batch queue cap.
    #[must_use]
    pub const fn max_intent_batches(self) -> usize {
        self.max_intent_batches.get()
    }

    /// Returns the sealed-batch queue cap as a non-zero proof for internal adapters.
    #[must_use]
    pub(crate) const fn max_intent_batches_limit(self) -> NonZeroUsize {
        self.max_intent_batches
    }
}

impl Default for PluginSchedulerPolicy {
    fn default() -> Self {
        Self {
            max_intent_batches: NonZeroUsize::new(MAX_PLUGIN_SCHEDULER_INTENT_BATCHES)
                .expect("default scheduler cap should be non-zero"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{MAX_PLUGIN_SCHEDULER_INTENT_BATCHES, PluginSchedulerPolicy};
    use crate::plugin::{PluginRegistryConfig, PluginRuntimeLimits};

    #[test]
    fn scheduler_policy_sums_enabled_plugin_bursts() {
        let registry = registry_with_limits([2, 3]);

        let policy = PluginSchedulerPolicy::from_validated_registry(&registry);

        assert_eq!(policy.max_intent_batches(), 5);
        assert_eq!(policy.max_intent_batches_limit().get(), 5);
    }

    #[test]
    fn scheduler_policy_ignores_disabled_plugins() {
        let config = PluginRegistryConfig {
            plugins: vec![
                plugin_config("enabled", true, 2),
                plugin_config("disabled", false, 7),
            ],
        };
        let registry = config.validate().expect("registry");

        let policy = PluginSchedulerPolicy::from_validated_registry(&registry);

        assert_eq!(policy.max_intent_batches(), 2);
    }

    #[test]
    fn scheduler_policy_uses_minimum_cap_without_enabled_plugins() {
        let registry = PluginRegistryConfig::default()
            .validate()
            .expect("registry");

        let policy = PluginSchedulerPolicy::from_validated_registry(&registry);

        assert_eq!(policy.max_intent_batches(), 1);
    }

    #[test]
    fn scheduler_policy_caps_registry_bursts() {
        let registry = registry_with_limits([
            MAX_PLUGIN_SCHEDULER_INTENT_BATCHES as u64,
            MAX_PLUGIN_SCHEDULER_INTENT_BATCHES as u64,
        ]);

        let policy = PluginSchedulerPolicy::from_validated_registry(&registry);

        assert_eq!(
            policy.max_intent_batches(),
            MAX_PLUGIN_SCHEDULER_INTENT_BATCHES
        );
    }

    fn registry_with_limits<const N: usize>(
        limits: [u64; N],
    ) -> crate::plugin::ValidatedPluginRegistry {
        let plugins = limits
            .into_iter()
            .enumerate()
            .map(|(index, max_intent_batches)| {
                plugin_config(&format!("plugin-{index}"), true, max_intent_batches)
            })
            .collect();
        PluginRegistryConfig { plugins }
            .validate()
            .expect("registry")
    }

    fn plugin_config(
        identity: &str,
        enabled: bool,
        max_intent_batches: u64,
    ) -> crate::plugin::PluginConfig {
        crate::plugin::PluginConfig {
            identity: identity.to_owned(),
            component_path: format!("plugins/{identity}.wasm").into(),
            enabled,
            runtime_limits: PluginRuntimeLimits {
                max_intent_batches,
                ..PluginRuntimeLimits::default()
            },
            ..crate::plugin::PluginConfig::default()
        }
    }
}