use arkhe_kernel::abi::{ArkheError, CapabilityMask, InstanceId, Principal, Tick};
use arkhe_kernel::state::traits::Action;
use arkhe_kernel::state::InstanceConfig;
use arkhe_kernel::{Kernel, StepReport, Wal};
use crate::wal_export::{BufferedWalSink, WalExportError, WalRecordSink};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WalSinkError {
#[error("WalRecord postcard encode failed: {0}")]
Encode(#[from] postcard::Error),
#[error("BufferedWalSink rejected record: {0}")]
Sink(#[from] WalExportError),
}
pub struct RuntimeService {
kernel: Kernel,
}
impl RuntimeService {
#[must_use]
pub fn new(world_id: [u8; 32], manifest_digest: [u8; 32]) -> Self {
Self {
kernel: Kernel::new_with_wal(world_id, manifest_digest),
}
}
pub fn register_action<A: Action>(&mut self) {
self.kernel.register_action::<A>();
}
pub fn create_instance(&mut self, config: InstanceConfig) -> InstanceId {
self.kernel.create_instance(config)
}
pub fn dispatch<A>(
&mut self,
instance: InstanceId,
principal: Principal,
action: &A,
at: Tick,
caps: CapabilityMask,
) -> Result<StepReport, ArkheError>
where
A: Action,
{
let bytes = action.canonical_bytes();
self.kernel
.submit(instance, principal, None, at, A::TYPE_CODE, bytes)?;
Ok(self.kernel.step(at, caps))
}
#[must_use]
pub fn export_wal(self) -> Option<Wal> {
self.kernel.export_wal()
}
}
pub fn wal_to_sink<W: std::io::Write>(
wal: &Wal,
sink: &mut BufferedWalSink<W>,
) -> Result<(), WalSinkError> {
for record in &wal.records {
let bytes = postcard::to_allocvec(record)?;
sink.append_record(&bytes)?;
}
sink.flush()?;
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use arkhe_kernel::abi::{Principal, Tick};
#[test]
fn fresh_service_has_zero_wal_records() {
let svc = RuntimeService::new([0x11u8; 32], [0x22u8; 32]);
assert_eq!(svc.kernel.wal_record_count(), Some(0));
}
#[test]
fn create_instance_grows_kernel() {
let mut svc = RuntimeService::new([0u8; 32], [0u8; 32]);
let _id = svc.create_instance(InstanceConfig::default());
assert_eq!(svc.kernel.instances_len(), 1);
}
#[test]
fn dispatch_unknown_instance_returns_instance_not_found() {
use arkhe_kernel::abi::EntityId;
use arkhe_kernel::state::{ActionCompute, ActionContext, Op};
use arkhe_kernel::ArkheAction;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, ArkheAction)]
#[arkhe(type_code = 0x0001_5101, schema_version = 1)]
struct NoopAction;
impl ActionCompute for NoopAction {
fn compute(&self, _ctx: &ActionContext<'_>) -> Vec<Op> {
vec![Op::SpawnEntity {
id: EntityId::new(1).unwrap(),
owner: Principal::System,
}]
}
}
let mut svc = RuntimeService::new([0u8; 32], [0u8; 32]);
svc.register_action::<NoopAction>();
let bogus = InstanceId::new(99).unwrap();
let result = svc.dispatch(
bogus,
Principal::System,
&NoopAction,
Tick(1),
CapabilityMask::SYSTEM,
);
assert!(matches!(result, Err(ArkheError::InstanceNotFound)));
}
#[test]
fn dispatch_happy_path_executes_one_action() {
use arkhe_kernel::abi::EntityId;
use arkhe_kernel::state::{ActionCompute, ActionContext, Op};
use arkhe_kernel::ArkheAction;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, ArkheAction)]
#[arkhe(type_code = 0x0001_5102, schema_version = 1)]
struct SpawnOne;
impl ActionCompute for SpawnOne {
fn compute(&self, _ctx: &ActionContext<'_>) -> Vec<Op> {
vec![Op::SpawnEntity {
id: EntityId::new(1).unwrap(),
owner: Principal::System,
}]
}
}
let mut svc = RuntimeService::new([0u8; 32], [0u8; 32]);
svc.register_action::<SpawnOne>();
let inst = svc.create_instance(InstanceConfig::default());
let report = svc
.dispatch(
inst,
Principal::System,
&SpawnOne,
Tick(0),
CapabilityMask::SYSTEM,
)
.expect("dispatch must succeed for live instance");
assert_eq!(report.actions_executed, 1);
assert_eq!(report.effects_applied, 1);
assert_eq!(report.effects_denied, 0);
}
#[test]
fn wal_to_sink_round_trips_single_record() {
use arkhe_kernel::abi::EntityId;
use arkhe_kernel::state::{ActionCompute, ActionContext, Op};
use arkhe_kernel::ArkheAction;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, ArkheAction)]
#[arkhe(type_code = 0x0001_5103, schema_version = 1)]
struct SpawnOne;
impl ActionCompute for SpawnOne {
fn compute(&self, _ctx: &ActionContext<'_>) -> Vec<Op> {
vec![Op::SpawnEntity {
id: EntityId::new(1).unwrap(),
owner: Principal::System,
}]
}
}
let mut svc = RuntimeService::new([0u8; 32], [0u8; 32]);
svc.register_action::<SpawnOne>();
let inst = svc.create_instance(InstanceConfig::default());
let _ = svc
.dispatch(
inst,
Principal::System,
&SpawnOne,
Tick(0),
CapabilityMask::SYSTEM,
)
.unwrap();
let wal = svc.export_wal().expect("WAL is configured");
assert_eq!(wal.records.len(), 1);
let mut buffer: Vec<u8> = Vec::new();
let mut sink = BufferedWalSink::new(&mut buffer);
wal_to_sink(&wal, &mut sink).expect("wal_to_sink must succeed");
assert!(!buffer.is_empty(), "sink writer must hold framed bytes");
assert!(
buffer.starts_with(&crate::wal_export::STREAM_HEADER_MAGIC),
"sink stream must begin with ARKHEXP1 magic",
);
}
#[test]
fn wal_to_sink_handles_multi_record_stream() {
use arkhe_kernel::abi::EntityId;
use arkhe_kernel::state::{ActionCompute, ActionContext, Op};
use arkhe_kernel::ArkheAction;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, ArkheAction)]
#[arkhe(type_code = 0x0001_5104, schema_version = 1)]
struct SpawnAt(u64);
impl ActionCompute for SpawnAt {
fn compute(&self, _ctx: &ActionContext<'_>) -> Vec<Op> {
vec![Op::SpawnEntity {
id: EntityId::new(self.0.max(1)).unwrap(),
owner: Principal::System,
}]
}
}
let mut svc = RuntimeService::new([0u8; 32], [0u8; 32]);
svc.register_action::<SpawnAt>();
let inst = svc.create_instance(InstanceConfig::default());
for i in 1..=3 {
svc.dispatch(
inst,
Principal::System,
&SpawnAt(i),
Tick(i),
CapabilityMask::SYSTEM,
)
.unwrap();
}
let wal = svc.export_wal().expect("WAL configured");
assert_eq!(wal.records.len(), 3);
let mut buffer: Vec<u8> = Vec::new();
let mut sink = BufferedWalSink::new(&mut buffer);
wal_to_sink(&wal, &mut sink).unwrap();
assert!(!buffer.is_empty());
assert!(buffer.starts_with(&crate::wal_export::STREAM_HEADER_MAGIC));
}
}