Skip to main content

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