worker_kv/
lib.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)]
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, thiserror::Error)]
170pub enum KvError {
171    #[error("js error: {0:?}")]
172    JavaScript(JsValue),
173    #[error("unable to serialize/deserialize: {0}")]
174    Serialization(serde_json::Error),
175    #[error("invalid kv store: {0}")]
176    InvalidKvStore(String),
177}
178
179impl From<KvError> for JsValue {
180    fn from(val: KvError) -> Self {
181        match val {
182            KvError::JavaScript(value) => value,
183            KvError::Serialization(e) => format!("KvError::Serialization: {e}").into(),
184            KvError::InvalidKvStore(binding) => {
185                format!("KvError::InvalidKvStore: {binding}").into()
186            }
187        }
188    }
189}
190
191impl From<JsValue> for KvError {
192    fn from(value: JsValue) -> Self {
193        Self::JavaScript(value)
194    }
195}
196
197impl From<serde_json::Error> for KvError {
198    fn from(value: serde_json::Error) -> Self {
199        Self::Serialization(value)
200    }
201}
202
203/// A trait for things that can be converted to [`wasm_bindgen::JsValue`] to be passed to the kv.
204pub trait ToRawKvValue {
205    fn raw_kv_value(&self) -> Result<JsValue, KvError>;
206}
207
208impl ToRawKvValue for str {
209    fn raw_kv_value(&self) -> Result<JsValue, KvError> {
210        Ok(JsValue::from(self))
211    }
212}
213
214impl<T: Serialize> ToRawKvValue for T {
215    fn raw_kv_value(&self) -> Result<JsValue, KvError> {
216        let value = serde_wasm_bindgen::to_value(self).map_err(JsValue::from)?;
217
218        if value.as_string().is_some() {
219            Ok(value)
220        } else if let Some(number) = value.as_f64() {
221            Ok(JsValue::from(number.to_string()))
222        } else if let Some(boolean) = value.as_bool() {
223            Ok(JsValue::from(boolean.to_string()))
224        } else {
225            js_sys::JSON::stringify(&value)
226                .map(JsValue::from)
227                .map_err(Into::into)
228        }
229    }
230}
231
232fn get(target: &JsValue, name: &str) -> Result<JsValue, JsValue> {
233    Reflect::get(target, &JsValue::from(name))
234}