#[cfg(not(test))]
use crate::db::commit::marker::COMMIT_LABEL;
use crate::error::InternalError;
use canic_cdk::structures::{DefaultMemoryImpl, memory::VirtualMemory};
use canic_memory::api::{MemoryApi, MemoryInspection};
use std::sync::OnceLock;
static COMMIT_STORE_ALLOCATION: OnceLock<CommitMemoryAllocation> = OnceLock::new();
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct CommitMemoryAllocation {
memory_id: u8,
stable_key: &'static str,
}
fn commit_memory_allocation() -> Result<CommitMemoryAllocation, InternalError> {
COMMIT_STORE_ALLOCATION
.get()
.copied()
.ok_or_else(InternalError::commit_memory_id_unconfigured)
}
pub(in crate::db::commit) fn configure_commit_memory_id(
memory_id: u8,
stable_key: &'static str,
) -> Result<u8, InternalError> {
if let Some(cached_id) = validate_cached_commit_memory_allocation(memory_id, stable_key)? {
return Ok(cached_id);
}
#[cfg(test)]
{
let _ = COMMIT_STORE_ALLOCATION.set(CommitMemoryAllocation {
memory_id,
stable_key,
});
Ok(memory_id)
}
#[cfg(not(test))]
{
MemoryApi::bootstrap_pending()
.map_err(InternalError::commit_memory_registry_init_failed)?;
validate_commit_slot_registration(memory_id, stable_key)?;
let _ = COMMIT_STORE_ALLOCATION.set(CommitMemoryAllocation {
memory_id,
stable_key,
});
Ok(memory_id)
}
}
fn validate_cached_commit_memory_allocation(
memory_id: u8,
stable_key: &'static str,
) -> Result<Option<u8>, InternalError> {
let Some(cached) = COMMIT_STORE_ALLOCATION.get() else {
return Ok(None);
};
if cached.memory_id != memory_id {
return Err(InternalError::commit_memory_id_mismatch(
cached.memory_id,
memory_id,
));
}
if cached.stable_key != stable_key {
return Err(InternalError::commit_memory_stable_key_mismatch(
cached.stable_key,
stable_key,
));
}
Ok(Some(cached.memory_id))
}
pub(super) fn commit_memory_handle() -> Result<VirtualMemory<DefaultMemoryImpl>, InternalError> {
let allocation = commit_memory_allocation()?;
#[cfg(test)]
{
Ok(crate::testing::test_memory(allocation.memory_id))
}
#[cfg(not(test))]
{
let owner = owner_for_memory_id(allocation.memory_id)?;
MemoryApi::register_with_key(
allocation.memory_id,
&owner,
COMMIT_LABEL,
allocation.stable_key,
)
.map_err(InternalError::commit_memory_id_registration_failed)
}
}
#[cfg(not(test))]
fn validate_commit_slot_registration(
memory_id: u8,
stable_key: &'static str,
) -> Result<(), InternalError> {
let mut commit_ids = MemoryApi::registered()
.into_iter()
.filter_map(|entry| (entry.stable_key == stable_key).then_some(entry.id))
.collect::<Vec<_>>();
commit_ids.sort_unstable();
commit_ids.dedup();
match commit_ids.as_slice() {
[] => Err(InternalError::commit_memory_stable_key_unregistered(
memory_id, stable_key,
)),
[registered_id] if *registered_id == memory_id => Ok(()),
[registered_id] => Err(InternalError::configured_commit_memory_id_mismatch(
memory_id,
*registered_id,
)),
_ => Err(InternalError::multiple_commit_memory_ids_registered(
commit_ids,
)),
}
}
#[cfg(not(test))]
fn owner_for_memory_id(memory_id: u8) -> Result<String, InternalError> {
Ok(inspect_memory(memory_id)?.owner)
}
#[cfg(not(test))]
fn inspect_memory(memory_id: u8) -> Result<MemoryInspection, InternalError> {
MemoryApi::inspect(memory_id)
.ok_or_else(|| InternalError::commit_memory_id_outside_reserved_ranges(memory_id))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::{ErrorClass, ErrorOrigin};
#[test]
fn owner_for_memory_inspection_returns_matching_owner() {
let inspection = MemoryInspection {
id: 12,
owner: "b".to_string(),
range: canic_memory::registry::MemoryRange { start: 11, end: 20 },
label: None,
stable_key: None,
schema_version: None,
schema_fingerprint: None,
};
let owner = inspection.owner;
assert_eq!(owner, "b");
}
#[test]
fn inspect_memory_missing_id_maps_to_out_of_range_error() {
let err = MemoryApi::inspect(30)
.ok_or_else(|| InternalError::commit_memory_id_outside_reserved_ranges(30))
.expect_err("id outside ranges must fail");
assert_eq!(err.class, ErrorClass::Unsupported);
assert_eq!(err.origin, ErrorOrigin::Store);
}
}