Skip to main content

meerkat_core/
blob.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5/// Canonical realm-local blob identifier.
6///
7/// The identifier is content-addressed, but storage and GC semantics remain
8/// realm-scoped.
9#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
10#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
11#[serde(transparent)]
12pub struct BlobId(String);
13
14impl BlobId {
15    pub fn new(value: impl Into<String>) -> Self {
16        Self(value.into())
17    }
18
19    pub fn as_str(&self) -> &str {
20        &self.0
21    }
22}
23
24impl fmt::Display for BlobId {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        f.write_str(self.as_str())
27    }
28}
29
30impl From<String> for BlobId {
31    fn from(value: String) -> Self {
32        Self(value)
33    }
34}
35
36impl From<&str> for BlobId {
37    fn from(value: &str) -> Self {
38        Self(value.to_string())
39    }
40}
41
42/// Durable image reference owned by transcript/runtime state.
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
45pub struct BlobRef {
46    pub blob_id: BlobId,
47    pub media_type: String,
48}
49
50/// Resolved blob bytes returned by the blob store.
51#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct BlobPayload {
54    pub blob_id: BlobId,
55    pub media_type: String,
56    /// Base64-encoded bytes.
57    pub data: String,
58}
59
60#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
61pub enum BlobStoreError {
62    #[error("blob not found: {0}")]
63    NotFound(BlobId),
64    #[error("blob store read failed: {0}")]
65    ReadFailed(String),
66    #[error("blob store write failed: {0}")]
67    WriteFailed(String),
68    #[error("blob store delete failed: {0}")]
69    DeleteFailed(String),
70    #[error("blob store unsupported: {0}")]
71    Unsupported(String),
72    #[error("blob store internal error: {0}")]
73    Internal(String),
74}
75
76#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
77#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
78pub trait BlobStore: Send + Sync {
79    async fn put_image(&self, media_type: &str, data: &str) -> Result<BlobRef, BlobStoreError>;
80
81    async fn get(&self, blob_id: &BlobId) -> Result<BlobPayload, BlobStoreError>;
82
83    async fn delete(&self, blob_id: &BlobId) -> Result<(), BlobStoreError>;
84
85    async fn exists(&self, blob_id: &BlobId) -> Result<bool, BlobStoreError> {
86        match self.get(blob_id).await {
87            Ok(_) => Ok(true),
88            Err(BlobStoreError::NotFound(_)) => Ok(false),
89            Err(err) => Err(err),
90        }
91    }
92
93    /// Whether the store is persistent across process restarts.
94    fn is_persistent(&self) -> bool;
95}