Skip to main content

omnia_wasi_blobstore/
host.rs

1//! # WASI Blobstore Service
2
3mod blobstore_impl;
4mod container_impl;
5pub mod default_impl;
6mod resource;
7mod types_impl;
8
9mod generated {
10    #![allow(missing_docs)]
11
12    pub type Error = String;
13
14    pub use super::{ContainerProxy, IncomingValue, OutgoingValue, StreamObjectNames};
15
16    wasmtime::component::bindgen!({
17        world: "imports",
18        path: "wit",
19        imports: {
20            default: store | tracing | trappable,
21        },
22        with: {
23            "wasi:io": wasmtime_wasi::p2::bindings::io,
24            "wasi:blobstore/types.incoming-value": IncomingValue,
25            "wasi:blobstore/types.outgoing-value": OutgoingValue,
26            "wasi:blobstore/container.container": ContainerProxy,
27            "wasi:blobstore/container.stream-object-names": StreamObjectNames,
28        },
29        trappable_error_type: {
30            "wasi:blobstore/types.error" => Error,
31        },
32    });
33}
34
35use std::fmt::Debug;
36use std::sync::Arc;
37
38use bytes::Bytes;
39pub use omnia::FutureResult;
40use omnia::{Host, Server, State};
41pub use resource::*;
42use wasmtime::component::{HasData, Linker, ResourceTable};
43use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
44
45pub use self::default_impl::BlobstoreDefault;
46pub use self::generated::wasi::blobstore::container::{ContainerMetadata, ObjectMetadata};
47pub use self::generated::wasi::blobstore::types::Error;
48use self::generated::wasi::blobstore::{blobstore, container, types};
49
50/// Incoming value for a blobstore operation.
51pub type IncomingValue = Bytes;
52/// Outgoing value for a blobstore operation.
53#[derive(Debug, Clone)]
54pub struct OutgoingValue {
55    pub(crate) pipe: MemoryOutputPipe,
56    pub(crate) write_body_taken: bool,
57    pub(crate) finished: bool,
58}
59
60impl OutgoingValue {
61    /// Create a new outgoing value with an in-memory buffer capacity.
62    #[must_use]
63    pub fn new(capacity: usize) -> Self {
64        Self {
65            pipe: MemoryOutputPipe::new(capacity),
66            write_body_taken: false,
67            finished: false,
68        }
69    }
70
71    pub(crate) const fn take_write_body(&mut self) -> std::result::Result<(), ()> {
72        if self.finished || self.write_body_taken {
73            return Err(());
74        }
75        self.write_body_taken = true;
76        Ok(())
77    }
78
79    pub(crate) const fn finalize(&mut self) -> std::result::Result<(), &'static str> {
80        if self.finished {
81            return Err("outgoing value already finished");
82        }
83        if !self.write_body_taken {
84            return Err("outgoing value write body was never requested");
85        }
86        self.finished = true;
87        Ok(())
88    }
89}
90/// Stream of object names with position tracking for paginated reads.
91pub struct StreamObjectNames {
92    /// The full list of object names in this stream.
93    pub(crate) names: Vec<String>,
94    /// Current read offset into `names`.
95    pub(crate) offset: usize,
96}
97
98impl StreamObjectNames {
99    /// Create a new stream from a complete list of object names.
100    #[must_use]
101    pub const fn new(names: Vec<String>) -> Self {
102        Self { names, offset: 0 }
103    }
104}
105
106/// Result type for blobstore operations.
107pub type Result<T> = anyhow::Result<T, Error>;
108
109/// Host-side service for `wasi:blobstore`.
110#[derive(Debug)]
111pub struct WasiBlobstore;
112
113impl HasData for WasiBlobstore {
114    type Data<'a> = WasiBlobstoreCtxView<'a>;
115}
116
117impl<T> Host<T> for WasiBlobstore
118where
119    T: WasiBlobstoreView + 'static,
120{
121    fn add_to_linker(linker: &mut Linker<T>) -> anyhow::Result<()> {
122        blobstore::add_to_linker::<_, Self>(linker, T::blobstore)?;
123        container::add_to_linker::<_, Self>(linker, T::blobstore)?;
124        Ok(types::add_to_linker::<_, Self>(linker, T::blobstore)?)
125    }
126}
127
128impl<S> Server<S> for WasiBlobstore where S: State {}
129
130/// A trait which provides internal WASI Blobstore state.
131///
132/// This is implemented by the `T` in `Linker<T>` — a single type shared across
133/// all WASI components for the runtime build.
134pub trait WasiBlobstoreView: Send {
135    /// Return a [`WasiBlobstoreCtxView`] from mutable reference to self.
136    fn blobstore(&mut self) -> WasiBlobstoreCtxView<'_>;
137}
138
139/// View into [`WasiBlobstoreCtx`] implementation and [`ResourceTable`].
140pub struct WasiBlobstoreCtxView<'a> {
141    /// Mutable reference to the WASI Blobstore context.
142    pub ctx: &'a mut dyn WasiBlobstoreCtx,
143
144    /// Mutable reference to table used to manage resources.
145    pub table: &'a mut ResourceTable,
146}
147
148/// A trait which provides internal WASI Blobstore context.
149///
150/// This is implemented by the resource-specific provider of Blobstore
151/// functionality. For example, an in-memory store, or a Redis-backed store.
152pub trait WasiBlobstoreCtx: Debug + Send + Sync + 'static {
153    /// Open a container.
154    fn create_container(&self, name: String) -> FutureResult<Arc<dyn Container>>;
155
156    /// Get a container.
157    fn get_container(&self, name: String) -> FutureResult<Arc<dyn Container>>;
158
159    /// Delete a container.
160    fn delete_container(&self, name: String) -> FutureResult<()>;
161
162    /// Check if a container exists.
163    fn container_exists(&self, name: String) -> FutureResult<bool>;
164}
165
166/// Implementation of the `WasiBlobstoreView` trait for the store context.
167#[macro_export]
168macro_rules! omnia_wasi_view {
169    ($store_ctx:ty, $field_name:ident) => {
170        impl omnia_wasi_blobstore::WasiBlobstoreView for $store_ctx {
171            fn blobstore(&mut self) -> omnia_wasi_blobstore::WasiBlobstoreCtxView<'_> {
172                omnia_wasi_blobstore::WasiBlobstoreCtxView {
173                    ctx: &mut self.$field_name,
174                    table: &mut self.table,
175                }
176            }
177        }
178    };
179}
180
181// impl<'a, T> CtxView<'a, T> for WasiBlobstore
182// where
183//     T: WasiBlobstoreCtx,
184// {
185//     fn ctx_view(ctx: &'a mut T, table: &'a mut ResourceTable) -> WasiBlobstoreCtxView<'a> {
186//         WasiBlobstoreCtxView { ctx, table }
187//     }
188// }
189
190// #[macro_export]
191// macro_rules! omnia_wasi_view {
192//     ($store_ctx:ty, $field_name:ident) => {
193//         impl View<WasiBlobstore, $store_ctx> for $store_ctx {
194//             fn data(&mut self) -> <WasiBlobstore as HasData>::Data<'_> {
195//                 WasiBlobstore::ctx_view(&mut self.$field_name, &mut self.table)
196//             }
197//         }
198//     };
199// }
200
201#[cfg(test)]
202mod tests {
203    use super::OutgoingValue;
204
205    #[test]
206    fn outgoing_value_write_body_is_one_shot() {
207        let mut outgoing = OutgoingValue::new(16);
208        assert_eq!(outgoing.take_write_body(), Ok(()));
209        assert_eq!(outgoing.take_write_body(), Err(()));
210    }
211
212    #[test]
213    fn outgoing_value_finish_requires_write_body() {
214        let mut outgoing = OutgoingValue::new(16);
215        assert_eq!(outgoing.finalize(), Err("outgoing value write body was never requested"));
216    }
217
218    #[test]
219    fn outgoing_value_finish_is_single_use() {
220        let mut outgoing = OutgoingValue::new(16);
221        assert_eq!(outgoing.take_write_body(), Ok(()));
222        assert_eq!(outgoing.finalize(), Ok(()));
223        assert_eq!(outgoing.finalize(), Err("outgoing value already finished"));
224    }
225}