crux_kv/
lib.rs

1//! A basic Key-Value store for use with Crux
2//!
3//! `crux_kv` allows Crux apps to store and retrieve arbitrary data by asking the Shell to
4//! persist the data using platform native capabilities (e.g. disk or web localStorage)
5
6pub mod command;
7pub mod error;
8pub mod value;
9
10use serde::{Deserialize, Serialize};
11
12use crux_core::capability::{CapabilityContext, Operation};
13
14use error::KeyValueError;
15use value::Value;
16
17/// Supported operations
18#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
19pub enum KeyValueOperation {
20    /// Read bytes stored under a key
21    Get { key: String },
22    /// Write bytes under a key
23    Set {
24        key: String,
25        #[serde(with = "serde_bytes")]
26        value: Vec<u8>,
27    },
28    /// Remove a key and its value
29    Delete { key: String },
30    /// Test if a key exists
31    Exists { key: String },
32    // List keys that start with a prefix, starting at the cursor
33    ListKeys {
34        /// The prefix to list keys for, or an empty string to list all keys
35        prefix: String,
36        /// The cursor to start listing from, or 0 to start from the beginning.
37        /// If there are more keys to list, the response will include a new cursor.
38        /// If there are no more keys, the response will include a cursor of 0.
39        /// The cursor is opaque to the caller, and should be passed back to the
40        /// `ListKeys` operation to continue listing keys.
41        /// If the cursor is not found for the specified prefix, the response will include
42        /// a `KeyValueError::CursorNotFound` error.
43        cursor: u64,
44    },
45}
46
47impl std::fmt::Debug for KeyValueOperation {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match self {
50            KeyValueOperation::Get { key } => f.debug_struct("Get").field("key", key).finish(),
51            KeyValueOperation::Set { key, value } => {
52                let body_repr = if let Ok(s) = std::str::from_utf8(value) {
53                    if s.len() < 50 {
54                        format!("\"{s}\"")
55                    } else {
56                        format!("\"{}\"...", s.chars().take(50).collect::<String>())
57                    }
58                } else {
59                    format!("<binary data - {} bytes>", value.len())
60                };
61                f.debug_struct("Set")
62                    .field("key", key)
63                    .field("value", &format_args!("{}", body_repr))
64                    .finish()
65            }
66            KeyValueOperation::Delete { key } => {
67                f.debug_struct("Delete").field("key", key).finish()
68            }
69            KeyValueOperation::Exists { key } => {
70                f.debug_struct("Exists").field("key", key).finish()
71            }
72            KeyValueOperation::ListKeys { prefix, cursor } => f
73                .debug_struct("ListKeys")
74                .field("prefix", prefix)
75                .field("cursor", cursor)
76                .finish(),
77        }
78    }
79}
80
81/// The result of an operation on the store.
82///
83/// Note: we can't use `Result` and `Option` here because generics are not currently
84/// supported across the FFI boundary, when using the builtin typegen.
85#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
86pub enum KeyValueResult {
87    Ok { response: KeyValueResponse },
88    Err { error: KeyValueError },
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub enum KeyValueResponse {
93    /// Response to a `KeyValueOperation::Get`,
94    /// returning the value stored under the key, which may be empty
95    Get { value: Value },
96    /// Response to a `KeyValueOperation::Set`,
97    /// returning the value that was previously stored under the key, may be empty
98    Set { previous: Value },
99    /// Response to a `KeyValueOperation::Delete`,
100    /// returning the value that was previously stored under the key, may be empty
101    Delete { previous: Value },
102    /// Response to a `KeyValueOperation::Exists`,
103    /// returning whether the key is present in the store
104    Exists { is_present: bool },
105    /// Response to a `KeyValueOperation::ListKeys`,
106    /// returning a list of keys that start with the prefix, and a cursor to continue listing
107    /// if there are more keys
108    ///
109    /// Note: the cursor is 0 if there are no more keys
110    ListKeys {
111        keys: Vec<String>,
112        /// The cursor to continue listing keys, or 0 if there are no more keys.
113        /// If the cursor is not found for the specified prefix, the response should instead
114        /// include a `KeyValueError::CursorNotFound` error.
115        next_cursor: u64,
116    },
117}
118
119impl Operation for KeyValueOperation {
120    type Output = KeyValueResult;
121
122    #[cfg(feature = "typegen")]
123    fn register_types(generator: &mut crux_core::typegen::TypeGen) -> crux_core::typegen::Result {
124        generator.register_type::<KeyValueResponse>()?;
125        generator.register_type::<KeyValueError>()?;
126        generator.register_type::<Value>()?;
127        generator.register_type::<Self>()?;
128        generator.register_type::<Self::Output>()?;
129        Ok(())
130    }
131}
132
133pub struct KeyValue<Ev> {
134    context: CapabilityContext<KeyValueOperation, Ev>,
135}
136
137impl<Ev> crux_core::Capability<Ev> for KeyValue<Ev> {
138    type Operation = KeyValueOperation;
139
140    type MappedSelf<MappedEv> = KeyValue<MappedEv>;
141
142    fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
143    where
144        F: Fn(NewEv) -> Ev + Send + Sync + 'static,
145        Ev: 'static,
146        NewEv: 'static + Send,
147    {
148        KeyValue::new(self.context.map_event(f))
149    }
150}
151
152impl<Ev> Clone for KeyValue<Ev> {
153    fn clone(&self) -> Self {
154        Self {
155            context: self.context.clone(),
156        }
157    }
158}
159
160impl<Ev> KeyValue<Ev>
161where
162    Ev: 'static,
163{
164    pub fn new(context: CapabilityContext<KeyValueOperation, Ev>) -> Self {
165        Self { context }
166    }
167
168    /// Read a value under `key`, will dispatch the event with a
169    /// `KeyValueResult::Get { value: Vec<u8> }` as payload
170    pub fn get<F>(&self, key: String, make_event: F)
171    where
172        F: FnOnce(Result<Option<Vec<u8>>, KeyValueError>) -> Ev + Send + Sync + 'static,
173    {
174        self.context.spawn({
175            let context = self.context.clone();
176            async move {
177                let response = get(&context, key).await;
178                context.update_app(make_event(response));
179            }
180        });
181    }
182
183    /// Read a value under `key`, while in an async context. This is used together with
184    /// [`crux_core::compose::Compose`].
185    ///
186    /// Returns the value stored under the key, or `None` if the key is not present.
187    pub async fn get_async(&self, key: String) -> Result<Option<Vec<u8>>, KeyValueError> {
188        get(&self.context, key).await
189    }
190
191    /// Set `key` to be the provided `value`. Typically the bytes would be
192    /// a value serialized/deserialized by the app.
193    ///
194    /// Will dispatch the event with a `KeyValueResult::Set { previous: Vec<u8> }` as payload
195    pub fn set<F>(&self, key: String, value: Vec<u8>, make_event: F)
196    where
197        F: FnOnce(Result<Option<Vec<u8>>, KeyValueError>) -> Ev + Send + Sync + 'static,
198    {
199        self.context.spawn({
200            let context = self.context.clone();
201            async move {
202                let response = set(&context, key, value).await;
203                context.update_app(make_event(response))
204            }
205        });
206    }
207
208    /// Set `key` to be the provided `value`, while in an async context. This is used together with
209    /// [`crux_core::compose::Compose`].
210    ///
211    /// Returns the previous value stored under the key, if any.
212    pub async fn set_async(
213        &self,
214        key: String,
215        value: Vec<u8>,
216    ) -> Result<Option<Vec<u8>>, KeyValueError> {
217        set(&self.context, key, value).await
218    }
219
220    /// Remove a `key` and its value, will dispatch the event with a
221    /// `KeyValueResult::Delete { previous: Vec<u8> }` as payload
222    pub fn delete<F>(&self, key: String, make_event: F)
223    where
224        F: FnOnce(Result<Option<Vec<u8>>, KeyValueError>) -> Ev + Send + Sync + 'static,
225    {
226        self.context.spawn({
227            let context = self.context.clone();
228            async move {
229                let response = delete(&context, key).await;
230                context.update_app(make_event(response))
231            }
232        });
233    }
234
235    /// Remove a `key` and its value, while in an async context. This is used together with
236    /// [`crux_core::compose::Compose`].
237    ///
238    /// Returns the previous value stored under the key, if any.
239    pub async fn delete_async(&self, key: String) -> Result<Option<Vec<u8>>, KeyValueError> {
240        delete(&self.context, key).await
241    }
242
243    /// Check to see if a `key` exists, will dispatch the event with a
244    /// `KeyValueResult::Exists { is_present: bool }` as payload
245    pub fn exists<F>(&self, key: String, make_event: F)
246    where
247        F: FnOnce(Result<bool, KeyValueError>) -> Ev + Send + Sync + 'static,
248    {
249        self.context.spawn({
250            let context = self.context.clone();
251            async move {
252                let response = exists(&context, key).await;
253                context.update_app(make_event(response))
254            }
255        });
256    }
257
258    /// Check to see if a `key` exists, while in an async context. This is used together with
259    /// [`crux_core::compose::Compose`].
260    ///
261    /// Returns `true` if the key exists, `false` otherwise.
262    pub async fn exists_async(&self, key: String) -> Result<bool, KeyValueError> {
263        exists(&self.context, key).await
264    }
265
266    /// List keys that start with the provided `prefix`, starting from the provided `cursor`.
267    /// Will dispatch the event with a `KeyValueResult::ListKeys { keys: Vec<String>, cursor: u64 }`
268    /// as payload.
269    ///
270    /// A cursor is an opaque value that points to the first key in the next page of keys.
271    ///
272    /// If the cursor is not found for the specified prefix, the response will include
273    /// a `KeyValueError::CursorNotFound` error.
274    ///
275    /// If the cursor is found the result will be a tuple of the keys and the next cursor
276    /// (if there are more keys to list, the cursor will be non-zero, otherwise it will be zero)
277    pub fn list_keys<F>(&self, prefix: String, cursor: u64, make_event: F)
278    where
279        F: FnOnce(Result<(Vec<String>, u64), KeyValueError>) -> Ev + Send + Sync + 'static,
280    {
281        self.context.spawn({
282            let context = self.context.clone();
283            async move {
284                let response = list_keys(&context, prefix, cursor).await;
285                context.update_app(make_event(response))
286            }
287        });
288    }
289
290    /// List keys that start with the provided `prefix`, starting from the provided `cursor`,
291    /// while in an async context. This is used together with [`crux_core::compose::Compose`].
292    ///
293    /// A cursor is an opaque value that points to the first key in the next page of keys.
294    ///
295    /// If the cursor is not found for the specified prefix, the response will include
296    /// a `KeyValueError::CursorNotFound` error.
297    ///
298    /// If the cursor is found the result will be a tuple of the keys and the next cursor
299    /// (if there are more keys to list, the cursor will be non-zero, otherwise it will be zero)
300    pub async fn list_keys_async(
301        &self,
302        prefix: String,
303        cursor: u64,
304    ) -> Result<(Vec<String>, u64), KeyValueError> {
305        list_keys(&self.context, prefix, cursor).await
306    }
307}
308
309async fn get<Ev: 'static>(
310    context: &CapabilityContext<KeyValueOperation, Ev>,
311    key: String,
312) -> Result<Option<Vec<u8>>, KeyValueError> {
313    context
314        .request_from_shell(KeyValueOperation::Get { key })
315        .await
316        .unwrap_get()
317}
318
319async fn set<Ev: 'static>(
320    context: &CapabilityContext<KeyValueOperation, Ev>,
321    key: String,
322    value: Vec<u8>,
323) -> Result<Option<Vec<u8>>, KeyValueError> {
324    context
325        .request_from_shell(KeyValueOperation::Set { key, value })
326        .await
327        .unwrap_set()
328}
329
330async fn delete<Ev: 'static>(
331    context: &CapabilityContext<KeyValueOperation, Ev>,
332    key: String,
333) -> Result<Option<Vec<u8>>, KeyValueError> {
334    context
335        .request_from_shell(KeyValueOperation::Delete { key })
336        .await
337        .unwrap_delete()
338}
339
340async fn exists<Ev: 'static>(
341    context: &CapabilityContext<KeyValueOperation, Ev>,
342    key: String,
343) -> Result<bool, KeyValueError> {
344    context
345        .request_from_shell(KeyValueOperation::Exists { key })
346        .await
347        .unwrap_exists()
348}
349
350async fn list_keys<Ev: 'static>(
351    context: &CapabilityContext<KeyValueOperation, Ev>,
352    prefix: String,
353    cursor: u64,
354) -> Result<(Vec<String>, u64), KeyValueError> {
355    context
356        .request_from_shell(KeyValueOperation::ListKeys { prefix, cursor })
357        .await
358        .unwrap_list_keys()
359}
360
361impl KeyValueResult {
362    fn unwrap_get(self) -> Result<Option<Vec<u8>>, KeyValueError> {
363        match self {
364            KeyValueResult::Ok { response } => match response {
365                KeyValueResponse::Get { value } => Ok(value.into()),
366                _ => {
367                    panic!("attempt to convert KeyValueResponse other than Get to Option<Vec<u8>>")
368                }
369            },
370            KeyValueResult::Err { error } => Err(error.clone()),
371        }
372    }
373
374    fn unwrap_set(self) -> Result<Option<Vec<u8>>, KeyValueError> {
375        match self {
376            KeyValueResult::Ok { response } => match response {
377                KeyValueResponse::Set { previous } => Ok(previous.into()),
378                _ => {
379                    panic!("attempt to convert KeyValueResponse other than Set to Option<Vec<u8>>")
380                }
381            },
382            KeyValueResult::Err { error } => Err(error.clone()),
383        }
384    }
385
386    fn unwrap_delete(self) -> Result<Option<Vec<u8>>, KeyValueError> {
387        match self {
388            KeyValueResult::Ok { response } => match response {
389                KeyValueResponse::Delete { previous } => Ok(previous.into()),
390                _ => panic!(
391                    "attempt to convert KeyValueResponse other than Delete to Option<Vec<u8>>"
392                ),
393            },
394            KeyValueResult::Err { error } => Err(error.clone()),
395        }
396    }
397
398    fn unwrap_exists(self) -> Result<bool, KeyValueError> {
399        match self {
400            KeyValueResult::Ok { response } => match response {
401                KeyValueResponse::Exists { is_present } => Ok(is_present),
402                _ => panic!("attempt to convert KeyValueResponse other than Exists to bool"),
403            },
404            KeyValueResult::Err { error } => Err(error.clone()),
405        }
406    }
407
408    fn unwrap_list_keys(self) -> Result<(Vec<String>, u64), KeyValueError> {
409        match self {
410            KeyValueResult::Ok { response } => match response {
411                KeyValueResponse::ListKeys {
412                    keys,
413                    next_cursor: cursor,
414                } => Ok((keys, cursor)),
415                _ => panic!(
416                    "attempt to convert KeyValueResponse other than ListKeys to (Vec<String>, u64)"
417                ),
418            },
419            KeyValueResult::Err { error } => Err(error.clone()),
420        }
421    }
422}
423
424#[cfg(test)]
425mod tests;