worker/kv/
mod.rs

1//! Bindings to Cloudflare Worker's [KV](https://developers.cloudflare.com/workers/runtime-apis/kv)
2//! to be used ***inside*** of a worker's context.
3//!
4//! # Example
5//! ```ignore
6//! let kv = KvStore::create("Example")?;
7//!
8//! // Insert a new entry into the kv.
9//! kv.put("example_key", "example_value")?
10//!     .metadata(vec![1, 2, 3, 4]) // Use some arbitrary serialiazable metadata
11//!     .execute()
12//!     .await?;
13//!
14//! // NOTE: kv changes can take a minute to become visible to other workers.
15//! // Get that same metadata.
16//! let (value, metadata) = kv.get("example_key").text_with_metadata::<Vec<usize>>().await?;
17//! ```
18#[forbid(missing_docs)]
19mod builder;
20
21pub use builder::*;
22
23use js_sys::{global, Function, Object, Promise, Reflect, Uint8Array};
24use serde::{Deserialize, Serialize};
25use serde_json::Value;
26use wasm_bindgen::JsValue;
27use wasm_bindgen_futures::JsFuture;
28
29/// A binding to a Cloudflare KvStore.
30#[derive(Clone, Debug)]
31pub struct KvStore {
32    pub(crate) this: Object,
33    pub(crate) get_function: Function,
34    pub(crate) get_with_meta_function: Function,
35    pub(crate) put_function: Function,
36    pub(crate) list_function: Function,
37    pub(crate) delete_function: Function,
38}
39
40// Allows for attachment to axum router, as Workers will never allow multithreading.
41unsafe impl Send for KvStore {}
42unsafe impl Sync for KvStore {}
43
44impl KvStore {
45    /// Creates a new [`KvStore`] with the binding specified in your `wrangler.toml`.
46    pub fn create(binding: &str) -> Result<Self, KvError> {
47        let this = get(&global(), binding)?;
48
49        // Ensures that the kv store exists.
50        if this.is_undefined() {
51            Err(KvError::InvalidKvStore(binding.into()))
52        } else {
53            Ok(Self {
54                get_function: get(&this, "get")?.into(),
55                get_with_meta_function: get(&this, "getWithMetadata")?.into(),
56                put_function: get(&this, "put")?.into(),
57                list_function: get(&this, "list")?.into(),
58                delete_function: get(&this, "delete")?.into(),
59                this: this.into(),
60            })
61        }
62    }
63
64    /// Creates a new [`KvStore`] with the binding specified in your `wrangler.toml`, using an
65    /// alternative `this` value for arbitrary binding contexts.
66    pub fn from_this(this: &JsValue, binding: &str) -> Result<Self, KvError> {
67        let this = get(this, binding)?;
68
69        // Ensures that the kv store exists.
70        if this.is_undefined() {
71            Err(KvError::InvalidKvStore(binding.into()))
72        } else {
73            Ok(Self {
74                get_function: get(&this, "get")?.into(),
75                get_with_meta_function: get(&this, "getWithMetadata")?.into(),
76                put_function: get(&this, "put")?.into(),
77                list_function: get(&this, "list")?.into(),
78                delete_function: get(&this, "delete")?.into(),
79                this: this.into(),
80            })
81        }
82    }
83
84    /// Fetches the value from the kv store by name.
85    pub fn get(&self, name: &str) -> GetOptionsBuilder {
86        GetOptionsBuilder {
87            this: self.this.clone(),
88            get_function: self.get_function.clone(),
89            get_with_meta_function: self.get_with_meta_function.clone(),
90            name: JsValue::from(name),
91            cache_ttl: None,
92            value_type: None,
93        }
94    }
95
96    /// Puts data into the kv store.
97    pub fn put<T: ToRawKvValue>(&self, name: &str, value: T) -> Result<PutOptionsBuilder, KvError> {
98        Ok(PutOptionsBuilder {
99            this: self.this.clone(),
100            put_function: self.put_function.clone(),
101            name: JsValue::from(name),
102            value: value.raw_kv_value()?,
103            expiration: None,
104            expiration_ttl: None,
105            metadata: None,
106        })
107    }
108
109    /// Puts the specified byte slice into the kv store.
110    pub fn put_bytes(&self, name: &str, value: &[u8]) -> Result<PutOptionsBuilder, KvError> {
111        let typed_array = Uint8Array::new_with_length(value.len() as u32);
112        typed_array.copy_from(value);
113        let value: JsValue = typed_array.buffer().into();
114        Ok(PutOptionsBuilder {
115            this: self.this.clone(),
116            put_function: self.put_function.clone(),
117            name: JsValue::from(name),
118            value,
119            expiration: None,
120            expiration_ttl: None,
121            metadata: None,
122        })
123    }
124
125    /// Lists the keys in the kv store.
126    pub fn list(&self) -> ListOptionsBuilder {
127        ListOptionsBuilder {
128            this: self.this.clone(),
129            list_function: self.list_function.clone(),
130            limit: None,
131            cursor: None,
132            prefix: None,
133        }
134    }
135
136    /// Deletes a key in the kv store.
137    pub async fn delete(&self, name: &str) -> Result<(), KvError> {
138        let name = JsValue::from(name);
139        let promise: Promise = self.delete_function.call1(&self.this, &name)?.into();
140        JsFuture::from(promise).await?;
141        Ok(())
142    }
143}
144
145/// The response for listing the elements in a KV store.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ListResponse {
148    /// A slice of all of the keys in the KV store.
149    pub keys: Vec<Key>,
150    /// If there are more keys that can be fetched using the response's cursor.
151    pub list_complete: bool,
152    /// A string used for paginating responses.
153    pub cursor: Option<String>,
154}
155
156/// The representation of a key in the KV store.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct Key {
159    /// The name of the key.
160    pub name: String,
161    /// When (expressed as a [unix timestamp](https://en.wikipedia.org/wiki/Unix_time)) the key
162    /// value pair will expire in the store.
163    pub expiration: Option<u64>,
164    /// All metadata associated with the key.
165    pub metadata: Option<Value>,
166}
167
168/// A simple error type that can occur during kv operations.
169#[derive(Debug)]
170pub enum KvError {
171    JavaScript(JsValue),
172    Serialization(serde_json::Error),
173    InvalidKvStore(String),
174}
175
176unsafe impl Send for KvError {}
177unsafe impl Sync for KvError {}
178
179impl std::fmt::Display for KvError {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        match self {
182            KvError::JavaScript(value) => write!(f, "js error: {value:?}"),
183            KvError::Serialization(e) => write!(f, "unable to serialize/deserialize: {e}"),
184            KvError::InvalidKvStore(binding) => write!(f, "invalid kv store: {binding}"),
185        }
186    }
187}
188
189impl std::error::Error for KvError {}
190
191impl From<KvError> for JsValue {
192    fn from(val: KvError) -> Self {
193        match val {
194            KvError::JavaScript(value) => value,
195            KvError::Serialization(e) => format!("KvError::Serialization: {e}").into(),
196            KvError::InvalidKvStore(binding) => {
197                format!("KvError::InvalidKvStore: {binding}").into()
198            }
199        }
200    }
201}
202
203impl From<JsValue> for KvError {
204    fn from(value: JsValue) -> Self {
205        Self::JavaScript(value)
206    }
207}
208
209impl From<serde_json::Error> for KvError {
210    fn from(value: serde_json::Error) -> Self {
211        Self::Serialization(value)
212    }
213}
214
215/// A trait for things that can be converted to [`wasm_bindgen::JsValue`] to be passed to the kv.
216pub trait ToRawKvValue {
217    fn raw_kv_value(&self) -> Result<JsValue, KvError>;
218}
219
220impl ToRawKvValue for str {
221    fn raw_kv_value(&self) -> Result<JsValue, KvError> {
222        Ok(JsValue::from(self))
223    }
224}
225
226impl<T: Serialize> ToRawKvValue for T {
227    fn raw_kv_value(&self) -> Result<JsValue, KvError> {
228        let value = serde_wasm_bindgen::to_value(self).map_err(JsValue::from)?;
229
230        if value.as_string().is_some() {
231            Ok(value)
232        } else if let Some(number) = value.as_f64() {
233            Ok(JsValue::from(number.to_string()))
234        } else if let Some(boolean) = value.as_bool() {
235            Ok(JsValue::from(boolean.to_string()))
236        } else {
237            js_sys::JSON::stringify(&value)
238                .map(JsValue::from)
239                .map_err(Into::into)
240        }
241    }
242}
243
244fn get(target: &JsValue, name: &str) -> Result<JsValue, JsValue> {
245    Reflect::get(target, &JsValue::from(name))
246}