ark-api 0.17.0-pre.15

Ark API
Documentation
//! # 📤 Behavior Controller API
//!
//! This API provides functionality to indirectly communicate with external behavior modules.
//!
//! Through this API, a user can retrieve reflection info of all loaded behavior modules, create new
//! behavior instances of a particular type, and send messages to behavior instances.
//!
//! ## Message passing
//!
//! Messages are the protocols through which behaviors communicate. At the lowest level they are
//! serializable Rust structures.
//!
//! The entire message passing flow between this API and behavior modules goes as follows:
//!
//! 1. A user creates [`IncomingMessage`]s and batch sends them to Ark through [`BehaviorController::instances_handle_messages`]
//! 2. Ark owns the actual behavior modules and will, in order, send the [`IncomingMessage`]s to the correct
//!    behavior instances inside external behavior modules
//! 3. A behavior instance will then process the just received message
//! 4. During the processing of a message, a behavior instance can create new [`OutgoingMessage`]s that will be returned to
//!    the user, which then decides what to do with it
//!
//! ## Aspects
//!
//! Aspects are used to associate data with actors, and share it between behaviors. Aspects are just data and
//! don't implement any logic. Through [`BehaviorController::aspect_upsert`], users can sync their aspect data with Ark.
//! Internally, Ark stores aspects in so called "aspect stores", keyed by aspect guid first to be able to iterate on
//! all actors that have a given aspect blazingly fast.
//!
//! ### Why aspects?
//!
//! As each behavior module is a separate Wasm module, behavior instances from separate behavior modules can't access
//! each others memory, meaning there is no direct sharing of state. While state could be asynchronously queried via
//! message passing, aspects allow for a more optimal route for sharing state.

mod ffi {
    pub use crate::ffi::behavior_controller_v0::{self as v0, *};
    pub use crate::ffi::behavior_controller_v1::{self as v1, *};
}

use crate::Error;
pub use ffi::{
    ActorId, AspectAddr, BehaviorMeta, BehaviorModuleId, BehaviorModuleMeta,
    BehaviorModuleRegistration, BehaviorRegistration, BehaviorTypeId, Guid, INVALID_GUID_COMPONENT,
};
use std::rc::Rc;
use std::task::Poll;

#[doc(hidden)]
pub use ffi::v1::API as FFI_API;

#[derive(Hash, PartialEq, Eq)]
struct BehaviorInstanceInner(ffi::BehaviorInstanceId);

impl Drop for BehaviorInstanceInner {
    fn drop(&mut self) {
        ffi::instance_destroy(self.0);
    }
}

/// Weak reference to a behavior instance that lives somewhere in an external behavior module.
///
/// Cloning will create another reference to the same behavior instance. If you want to do a deep
/// clone that creates a new instance, use the `BehaviorInstance::deep_clone` function.
///
/// The instance is destroyed when all its weak references are dropped.
#[derive(Clone, Hash, PartialEq, Eq)]
pub struct BehaviorInstance(Rc<BehaviorInstanceInner>);

impl BehaviorInstance {
    /// Create a behavior instance of the specific type with the provided construction parameters
    pub fn new(type_id: BehaviorTypeId, params: &str) -> Self {
        Self(Rc::new(BehaviorInstanceInner(ffi::instance_create(
            type_id.0, params,
        ))))
    }

    /// Restore a behavior instance by deserializing it from a persistent representation
    pub fn restore(type_id: BehaviorTypeId, bytes: &[u8]) -> Self {
        Self(Rc::new(BehaviorInstanceInner(ffi::instance_restore(
            type_id.0, bytes,
        ))))
    }

    /// Creates a clone of the current instance, and returns a reference to the newly created
    /// one.
    pub fn deep_clone(instance: &Self) -> Self {
        Self(Rc::new(BehaviorInstanceInner(ffi::instance_clone(
            instance.0 .0,
        ))))
    }

    /// Persist a behavior instance by serializing it to a blob
    pub fn persist(&self) -> Vec<u8> {
        ffi::instance_persist(self.0 .0)
    }

    /// Retrieve the [`BehaviorTypeId`] of this instance
    pub fn type_id(&self) -> BehaviorTypeId {
        BehaviorTypeId((self.0 .0 >> 32) as ffi::BehaviorTypeIdInner)
    }
}

impl std::fmt::Debug for BehaviorInstance {
    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(fmt, "BehaviorInstance({})", self.0 .0)
    }
}

/// A command used for [`BehaviorController::aspect_upsert`] to issue an aspect insertion/update
/// that needs to be synced with the host
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct AspectUpsert<'a> {
    /// The aspect's address, comprising of an aspect guid and an actor id
    pub addr: AspectAddr,
    /// Serialized aspect payload
    pub serialized_aspect: &'a [u8],
}

impl<'a> From<AspectUpsert<'a>> for ffi::AspectUpsert {
    fn from(upsert: AspectUpsert<'a>) -> Self {
        Self::new(upsert.addr, upsert.serialized_aspect)
    }
}

/// Target address of an incoming (controller -> behavior module) message.
#[derive(Debug, Eq, PartialEq)]
enum IncomingMessageAddr {
    /// This message is targeted at a specific behavior instance.
    Behavior {
        actor_id: ActorId,
        instance: BehaviorInstance,
    },
    /// This message is targeted at the module itself, but can also target a specific behavior
    /// instance.
    Module {
        module_id: u16,
        instance: Option<BehaviorInstance>,
    },
}

/// Represents a message to be sent to a specific behavior instance for processing
///
/// It is "incoming" from the perspective of the behavior instance
#[derive(Debug, Eq, PartialEq)]
pub struct IncomingMessage<'a> {
    /// The target address of this message
    addr: IncomingMessageAddr,
    /// Serialized message payload
    pub serialized_message: &'a [u8],
}

impl<'a> IncomingMessage<'a> {
    /// Creates a new message to a specific behavior instance.
    pub fn new_to_instance(
        instance: BehaviorInstance,
        actor_id: ActorId,
        serialized_message: &'a [u8],
    ) -> Self {
        Self {
            addr: IncomingMessageAddr::Behavior { instance, actor_id },
            serialized_message,
        }
    }

    /// Creates a new message to a specific behavior module, represented by this behavior type.
    pub fn new_to_module(
        type_id: BehaviorTypeId,
        instance: Option<BehaviorInstance>,
        serialized_message: &'a [u8],
    ) -> Self {
        let module_id = (type_id.0 >> 16) as u16;

        if let Some(ref instance) = instance {
            // Sanity-check: the instance's module handle must be the same as the one from the behavior
            // type.
            assert_eq!(
                instance.0 .0 >> 48,
                u64::from(module_id),
                "behavior type and behavior instance must refer to the same behavior module"
            );
        }

        Self {
            addr: IncomingMessageAddr::Module {
                module_id,
                instance,
            },
            serialized_message,
        }
    }
}

impl<'a> From<&IncomingMessage<'a>> for ffi::IncomingMessage {
    fn from(msg: &IncomingMessage<'a>) -> Self {
        let (actor_id, instance_id) = match &msg.addr {
            IncomingMessageAddr::Behavior { actor_id, instance } => (*actor_id, instance.0 .0),
            IncomingMessageAddr::Module {
                module_id,
                instance,
            } => {
                let instance = match instance {
                    Some(instance) => instance.0 .0,
                    None => {
                        (u64::from(*module_id) << 48) | ffi::INCOMING_MESSAGE_NO_INSTANCE_SENTINEL
                    }
                };
                (ffi::CONTROLLER_SENTINEL_ACTOR_ID, instance)
            }
        };
        Self::new(instance_id, actor_id, msg.serialized_message)
    }
}

/// A response message received from a behavior module.
///
/// Can be targeted to an actor itself, or to the behavior controller itself as a system entity.
#[derive(Clone, Debug, Eq, PartialEq, Copy)]
pub enum OutgoingMessageAddr {
    /// Response targeted at a specific actor and behavior.
    Actor {
        /// Actor identifier.
        actor_id: ActorId,
        /// Behavior unique identifier encoded as a GUID; may not be present if the message is
        /// addressed to all the actor's behaviors.
        behavior_guid: Option<u128>,
    },
    /// Response targeted at the controller module itself.
    Controller,
}

impl From<&ffi::OutgoingMessageAddr> for OutgoingMessageAddr {
    fn from(addr: &ffi::OutgoingMessageAddr) -> Self {
        if addr.to_actor_id == ffi::CONTROLLER_SENTINEL_ACTOR_ID {
            Self::Controller
        } else {
            Self::Actor {
                actor_id: addr.to_actor_id,
                behavior_guid: addr.guid(),
            }
        }
    }
}

/// An outgoing message generated by a behavior instance during message processing
///
/// See [`OutgoingMessages`] on how to retrieve individual messages returned
/// from [`BehaviorController::instances_handle_messages`]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OutgoingMessage<'a> {
    addr: OutgoingMessageAddr,
    serialized_message: &'a [u8],
}

impl<'a> OutgoingMessage<'a> {
    /// Retrieve the outgoing message's address.
    pub fn addr(&self) -> OutgoingMessageAddr {
        self.addr
    }
    /// Retrieve the outgoing message's serialized bytes as a slice.
    pub fn msg(&self) -> &'a [u8] {
        self.serialized_message
    }
}

/// Collection of outgoing messages returned from [`BehaviorController::instances_handle_messages`]
///
/// Individual messages can be iterated through with [`OutgoingMessages::iter`]
pub struct OutgoingMessages {
    bytes: Vec<u8>,
}

impl OutgoingMessages {
    /// Returns an iterator over the outgoing messages
    pub fn iter(&self) -> OutgoingMessagesIter<'_> {
        OutgoingMessagesIter {
            outgoing_messages: self,
            offset: 0,
        }
    }

    /// Read the [`ffi::OutgoingMessage`] from the byte stream
    fn read_outgoing_msg_ffi(bytes: &[u8], offset: &mut usize) -> Option<ffi::OutgoingMessage> {
        const OUTGOING_MSG_SIZE: usize = std::mem::size_of::<ffi::OutgoingMessage>();

        let result = bytes
            .get((*offset)..(*offset + OUTGOING_MSG_SIZE))
            .map(|slice| unsafe {
                std::mem::transmute::<[u8; OUTGOING_MSG_SIZE], ffi::OutgoingMessage>(
                    slice.try_into().unwrap(),
                )
            });
        *offset += OUTGOING_MSG_SIZE;
        result
    }

    fn read_outgoing_message<'a>(
        bytes: &'a [u8],
        offset: &mut usize,
    ) -> Option<OutgoingMessage<'a>> {
        Self::read_outgoing_msg_ffi(bytes, offset).map(|outgoing| {
            let slice = bytes
                .get((*offset)..((*offset) + outgoing.serialized_message_len as usize))
                .unwrap();
            *offset += outgoing.serialized_message_len as usize;

            OutgoingMessage {
                addr: OutgoingMessageAddr::from(&outgoing.addr),
                serialized_message: slice,
            }
        })
    }
}

/// Iterator for [`OutgoingMessage`] stored in [`OutgoingMessages`]
///
/// Can only be created through [`OutgoingMessages::iter`]
pub struct OutgoingMessagesIter<'a> {
    outgoing_messages: &'a OutgoingMessages,
    offset: usize,
}

impl<'a> Iterator for OutgoingMessagesIter<'a> {
    type Item = OutgoingMessage<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        OutgoingMessages::read_outgoing_message(&self.outgoing_messages.bytes, &mut self.offset)
    }
}

/// A future for loading a module from the module store.
///
/// Can be retrieved from [`BehaviorController::load_behavior_module`].
///
/// The future returns a serialized JSON vector of bytes containing the
/// external registration info for the loaded module.
///
/// See [`BehaviorController::list_modules`] for details on how to deserialize
/// the registration info.
pub struct LoadModuleFuture {
    handle: ffi::LoadModuleHandle,
}

impl std::future::Future for LoadModuleFuture {
    type Output = Result<Vec<u8>, Error>;

    fn poll(
        self: std::pin::Pin<&mut Self>,
        _cx: &mut std::task::Context<'_>,
    ) -> Poll<Self::Output> {
        match ffi::retrieve_module(self.handle) {
            Ok(bytes) => Poll::Ready(Ok(bytes)),
            Err(crate::ErrorCode::Unavailable) => Poll::Pending,
            Err(error_code) => Poll::Ready(Err(Error::from(error_code))),
        }
    }
}

/// Future returned from [`BehaviorController::list_behavior_modules`] that can
/// be used to retrieve a list of behavior modules in a module store
pub struct ListModulesFuture {
    handle: ffi::ListModulesHandle,
}

impl std::future::Future for ListModulesFuture {
    type Output = Result<Vec<u8>, Error>;

    fn poll(
        self: std::pin::Pin<&mut Self>,
        _cx: &mut std::task::Context<'_>,
    ) -> Poll<Self::Output> {
        match ffi::retrieve_module_list(self.handle) {
            Ok(bytes) => Poll::Ready(Ok(bytes)),
            Err(crate::ErrorCode::Unavailable) => Poll::Pending,
            Err(error_code) => Poll::Ready(Err(Error::from(error_code))),
        }
    }
}

/// The `BehaviorController` API provides functionality for interacting with external behavior modules
#[derive(Copy, Clone)]
pub struct BehaviorController {}

impl BehaviorController {
    /// Returns a serialized JSON vector of bytes with a list of external registration info.
    ///
    /// Only registration info from behavior modules listed in a Cargo.toml's Ark annotation will be returned
    ///
    /// # Example
    ///
    /// At a bare minimum, the JSON vector can be deserialized into a vector of [`BehaviorModuleRegistration`]:
    ///
    /// ```ignore
    /// use ark_api::behavior_controller::BehaviorModuleRegistration;
    ///
    /// let list: Vec<BehaviorModuleRegistration> = serde_json::from_slice(behavior_controller().list_modules()).unwrap();
    /// ```
    ///
    /// If behavior modules were registered with a custom registration format they can be deserialized similarly:
    ///
    /// ```ignore
    /// use ark_api::behavior_controller::BehaviorModuleRegistration;
    ///
    /// // Custom data format used in behavior module registration
    /// #[derive(serde::Deserialize)]
    /// pub struct CustomBehaviorInfo {
    ///    #[serde(flatten)]
    ///    pub info: BehaviorModuleRegistration,
    ///
    ///    pub port_names: Vec<String>,
    ///    pub tag_names: Vec<String>,
    /// }
    ///
    /// let list: Vec<CustomBehaviorInfo> = serde_json::from_slice(behavior_controller().list_modules()).unwrap();
    /// ```
    pub fn list_modules(&self) -> Vec<u8> {
        ffi::list_modules()
    }

    /// Starts an asynchronous retrieval of a list of behavior modules from
    /// the connected remote module store.
    ///
    /// The completed future returns a serialized JSON vector of bytes with
    /// a list of [`BehaviorModuleMeta`](ffi::v1::BehaviorModuleMeta)
    ///
    /// # Example
    ///
    /// ```ignore
    /// use ark_api::behavior_controller::BehaviorModuleMeta;
    ///
    /// let json_bytes = behavior_controller().list_behavior_modules().await?;
    /// let metadatas: Vec<BehaviorModuleMeta> = serde_json::from_slice(&json_bytes).unwrap();
    /// ```
    #[inline]
    pub fn list_behavior_modules(&self) -> ListModulesFuture {
        ListModulesFuture {
            handle: ffi::list_behavior_modules(),
        }
    }

    /// Have the specified instances process the provided messages
    ///
    /// This is key functionality of the Behavior Controller API as it will kick off processing
    /// of messages for behavior instances in external behavior modules.
    ///
    /// The following will happen after a call to `instances_handle_messages`:
    ///
    /// 1. In order, Ark will extract the [`IncomingMessage`]'s address and send the message data to the
    ///    correct behavior instance owned by some external behavior module.
    /// 2. A behavior instance will then process the just received message.
    /// 3. During the processing of a message, a behavior instance can create new [`OutgoingMessage`]s that
    ///    will be returned to the user of this API.
    ///
    /// The user can then turn outgoing messages into incoming messages and repeat the above process.
    pub fn instances_handle_messages(&self, messages: &[IncomingMessage<'_>]) -> OutgoingMessages {
        let message_data = messages
            .iter()
            .map(ffi::IncomingMessage::from)
            .collect::<Vec<ffi::IncomingMessage>>();

        OutgoingMessages {
            bytes: ffi::instances_handle_messages(&message_data),
        }
    }

    /// Requests multiple insertions or updates in aspect stores; if an aspect doesn't exist, it's seamlessly
    /// created on the host
    pub fn aspect_upsert(&self, upserts: &[AspectUpsert<'_>]) {
        let upserts = upserts
            .iter()
            .copied()
            .map(ffi::AspectUpsert::from)
            .collect::<Vec<ffi::AspectUpsert>>();

        ffi::aspect_upsert(&upserts);
    }

    /// Requests multiple aspect removals. If the aspect store doesn't exist, nothing happens
    pub fn aspect_remove(&self, removes: &[AspectAddr]) {
        ffi::aspect_remove(removes);
    }

    /// Reset all existing aspect stores to their initial states
    pub fn aspect_reset_all(&self) {
        ffi::aspect_reset_all();
    }

    /// Publishes the module with the given ID and returns a possibly different ID of the published version.
    ///
    /// If the module is already published nothing will happen and the function will return the same ID as the one
    /// provided as an argument.
    pub fn publish(&self, id: &BehaviorModuleId) -> Result<BehaviorModuleId, Error> {
        Ok(BehaviorModuleId::new(ffi::publish(id)?))
    }

    /// Starts an asynchronous retrieval of a behavior module from the module store
    pub fn load_behavior_module(&self, id: &BehaviorModuleId) -> Result<LoadModuleFuture, Error> {
        let handle = ffi::load_module(id)?;
        Ok(LoadModuleFuture { handle })
    }
}