momento_functions_host/cache.rs
1//! Host interfaces for working with Momento Cache apis
2
3use std::time::Duration;
4
5use crate::encoding::{Encode, EncodeError, Extract, ExtractError};
6use momento_functions_wit::host::momento::functions::cache_list;
7use momento_functions_wit::host::momento::functions::cache_scalar;
8
9pub use cache_scalar::GetWithHashFound;
10pub use cache_scalar::GetWithHashResult;
11pub use cache_scalar::SetIfCondition;
12pub use cache_scalar::SetIfHashCondition;
13pub use cache_scalar::SetIfHashResult;
14pub use cache_scalar::SetIfResult;
15use momento_functions_wit::host::momento::functions::cache_list::FetchResponse;
16
17/// An error occurred when setting a value in the cache.
18#[derive(thiserror::Error, Debug)]
19pub enum CacheSetError<E: EncodeError> {
20 /// The provided value could not be encoded.
21 #[error("Failed to encode value.")]
22 EncodeFailed {
23 /// The underlying encoding error.
24 cause: E,
25 },
26 /// An error occurred when calling the host cache function.
27 #[error(transparent)]
28 CacheError(#[from] cache_scalar::Error),
29}
30
31/// An error occurred when getting a value from the cache.
32#[derive(thiserror::Error, Debug)]
33pub enum CacheGetError<E: ExtractError> {
34 /// The value could not be extracted with the provided implementation.
35 #[error("Failed to extract value.")]
36 ExtractFailed {
37 /// The underlying error.
38 cause: E,
39 },
40 /// An error occurred when calling the host cache function.
41 #[error(transparent)]
42 CacheError(#[from] cache_scalar::Error),
43}
44/// Get a value from the cache.
45///
46/// Examples:
47/// ________
48/// Bytes:
49/// ```rust,no_run
50/// # use momento_functions_host::cache;
51/// match cache::get::<Vec<u8>>("my_key") {
52/// Ok(Some(value)) => { /* use value */ }
53/// Ok(None) => { /* key not found */ }
54/// Err(e) => eprintln!("cache get failed: {e}"),
55/// }
56/// ```
57/// ________
58/// Json:
59/// ```rust,no_run
60/// # use momento_functions_host::cache;
61/// use momento_functions_host::encoding::Json;
62///
63/// #[derive(serde::Deserialize)]
64/// struct MyStruct {
65/// message: String
66/// }
67///
68/// match cache::get::<Json<MyStruct>>("my_key") {
69/// Ok(Some(Json(value))) => { /* use value */ }
70/// Ok(None) => { /* key not found */ }
71/// Err(e) => eprintln!("cache get failed: {e}"),
72/// }
73/// ```
74pub fn get<T: Extract>(key: impl AsRef<[u8]>) -> Result<Option<T>, CacheGetError<T::Error>> {
75 match cache_scalar::get(key.as_ref())? {
76 Some(v) => T::extract(v)
77 .map(Some)
78 .map_err(|e| CacheGetError::ExtractFailed { cause: e }),
79 None => Ok(None),
80 }
81}
82
83/// Set a value in the cache with a time-to-live.
84///
85/// Examples:
86/// ________
87/// Bytes:
88/// ```rust,no_run
89/// # use momento_functions_host::cache;
90/// # use std::time::Duration;
91/// match cache::set(
92/// "my_key",
93/// b"hello".to_vec(),
94/// Duration::from_secs(60),
95/// ) {
96/// Ok(()) => {}
97/// Err(e) => eprintln!("cache set failed: {e}"),
98/// }
99/// ```
100/// ________
101/// Json:
102/// ```rust,no_run
103/// # use momento_functions_host::cache;
104/// # use std::time::Duration;
105/// use momento_functions_host::encoding::Json;
106///
107/// #[derive(serde::Serialize)]
108/// struct MyStruct {
109/// hello: String
110/// }
111///
112/// match cache::set(
113/// "my_key",
114/// Json(MyStruct { hello: "hello".to_string() }),
115/// Duration::from_secs(60),
116/// ) {
117/// Ok(()) => {}
118/// Err(e) => eprintln!("cache set failed: {e}"),
119/// }
120/// ```
121pub fn set<E: Encode>(
122 key: impl AsRef<[u8]>,
123 value: E,
124 ttl: Duration,
125) -> Result<(), CacheSetError<E::Error>> {
126 cache_scalar::set(
127 key.as_ref(),
128 &value
129 .try_serialize()
130 .map_err(|e| CacheSetError::EncodeFailed { cause: e })?
131 .into(),
132 saturate_ttl(ttl),
133 )
134 .map_err(Into::into)
135}
136
137fn saturate_ttl(ttl: Duration) -> u64 {
138 ttl.as_millis().clamp(0, u64::MAX as u128) as u64
139}
140
141/// An error occurred when conditionally setting a value in the cache.
142#[derive(thiserror::Error, Debug)]
143pub enum CacheSetIfError<E: EncodeError> {
144 /// The provided value could not be encoded.
145 #[error("Failed to encode value.")]
146 EncodeFailed {
147 /// The underlying encoding error.
148 cause: E,
149 },
150 /// An error occurred when calling the host cache function.
151 #[error(transparent)]
152 CacheError(#[from] cache_scalar::Error),
153}
154
155/// An error occurred when deleting a value from the cache.
156#[derive(thiserror::Error, Debug)]
157pub enum CacheDeleteError {
158 /// An error occurred when calling the host cache function.
159 #[error(transparent)]
160 CacheError(#[from] cache_scalar::Error),
161}
162
163/// Conditionally set a value in the cache based on a condition.
164///
165/// Examples:
166/// ________
167/// Set only if absent:
168/// ```rust,no_run
169/// # use momento_functions_host::cache;
170/// # use momento_functions_host::cache::{SetIfCondition, SetIfResult};
171/// # use std::time::Duration;
172/// match cache::set_if(
173/// "my_key",
174/// b"hello".to_vec(),
175/// Duration::from_secs(60),
176/// SetIfCondition::Absent,
177/// ) {
178/// Ok(SetIfResult::Stored) => {
179/// // Do something
180/// }
181/// Ok(SetIfResult::NotStored) => {
182/// // Do something else
183/// }
184/// Err(e) => eprintln!("set_if failed: {e}"),
185/// }
186/// ```
187/// ________
188/// Set only if present:
189/// ```rust,no_run
190/// # use momento_functions_host::cache;
191/// # use momento_functions_host::cache::SetIfCondition;
192/// # use std::time::Duration;
193/// match cache::set_if(
194/// "my_key",
195/// b"updated".to_vec(),
196/// Duration::from_secs(60),
197/// SetIfCondition::Present,
198/// ) {
199/// Ok(result) => { /* inspect result */ }
200/// Err(e) => eprintln!("set_if failed: {e}"),
201/// }
202/// ```
203/// ________
204/// Set only if equal to a specific value:
205/// ```rust,no_run
206/// # use momento_functions_host::cache;
207/// # use momento_functions_host::cache::SetIfCondition;
208/// # use std::time::Duration;
209/// match cache::set_if(
210/// "my_key",
211/// b"new_value".to_vec(),
212/// Duration::from_secs(60),
213/// SetIfCondition::Equal(b"old_value".to_vec()),
214/// ) {
215/// Ok(result) => { /* inspect result */ }
216/// Err(e) => eprintln!("set_if failed: {e}"),
217/// }
218/// ```
219pub fn set_if<E: Encode>(
220 key: impl AsRef<[u8]>,
221 value: E,
222 ttl: Duration,
223 condition: SetIfCondition,
224) -> Result<SetIfResult, CacheSetIfError<E::Error>> {
225 cache_scalar::set_if(
226 key.as_ref(),
227 &value
228 .try_serialize()
229 .map_err(|e| CacheSetIfError::EncodeFailed { cause: e })?
230 .into(),
231 saturate_ttl(ttl),
232 &condition,
233 )
234 .map_err(Into::into)
235}
236
237/// Delete a value from the cache.
238///
239/// Note: This operation is idempotent. Deleting a key that does not exist
240/// will not return an error.
241///
242/// Examples:
243/// ________
244/// ```rust,no_run
245/// # use momento_functions_host::cache;
246/// match cache::delete("my_key") {
247/// Ok(()) => {}
248/// Err(e) => eprintln!("cache delete failed: {e}"),
249/// }
250/// ```
251pub fn delete(key: impl AsRef<[u8]>) -> Result<(), CacheDeleteError> {
252 cache_scalar::delete(key.as_ref()).map_err(Into::into)
253}
254
255/// An error occurred when getting a value with its hash from the cache.
256#[derive(thiserror::Error, Debug)]
257pub enum CacheGetWithHashError<E: ExtractError> {
258 /// The value could not be extracted with the provided implementation.
259 #[error("Failed to extract value.")]
260 ExtractFailed {
261 /// The underlying error.
262 cause: E,
263 },
264 /// An error occurred when calling the host cache function.
265 #[error(transparent)]
266 CacheError(#[from] cache_scalar::Error),
267}
268
269/// An error occurred when conditionally setting a value based on hash comparison.
270#[derive(thiserror::Error, Debug)]
271pub enum CacheSetIfHashError<E: EncodeError> {
272 /// The provided value could not be encoded.
273 #[error("Failed to encode value.")]
274 EncodeFailed {
275 /// The underlying encoding error.
276 cause: E,
277 },
278 /// An error occurred when calling the host cache function.
279 #[error(transparent)]
280 CacheError(#[from] cache_scalar::Error),
281}
282
283/// A value retrieved from the cache along with its hash.
284pub struct GetWithHashValue<T> {
285 /// The extracted value.
286 pub value: T,
287 /// The hash of the value.
288 pub hash: Vec<u8>,
289}
290
291/// Get a value from the cache along with its hash.
292///
293/// The hash can be used with [`set_if_hash`] to perform conditional updates
294/// based on whether the value has changed since it was read.
295///
296/// Examples:
297/// ________
298/// ```rust,no_run
299/// # use momento_functions_host::cache;
300/// match cache::get_with_hash::<Vec<u8>>("my_key") {
301/// Ok(Some(entry)) => {
302/// log::info!("Value: {:?}, Hash: {:?}", entry.value, entry.hash);
303/// }
304/// Ok(None) => { /* key not found */ }
305/// Err(e) => eprintln!("get_with_hash failed: {e}"),
306/// }
307/// ```
308pub fn get_with_hash<T: Extract>(
309 key: impl AsRef<[u8]>,
310) -> Result<Option<GetWithHashValue<T>>, CacheGetWithHashError<T::Error>> {
311 match cache_scalar::get_with_hash(key.as_ref())? {
312 GetWithHashResult::Found(found) => {
313 let value = T::extract(found.value)
314 .map_err(|e| CacheGetWithHashError::ExtractFailed { cause: e })?;
315 Ok(Some(GetWithHashValue {
316 value,
317 hash: found.hash,
318 }))
319 }
320 GetWithHashResult::Missing => Ok(None),
321 }
322}
323
324/// Conditionally set a value in the cache based on a hash comparison.
325///
326/// This is useful for optimistic concurrency control where you want to update
327/// a value only if it hasn't changed since you last read it, without needing
328/// to compare the full value.
329///
330/// Examples:
331/// ________
332/// Update only if the hash matches (value hasn't changed):
333/// ```rust,no_run
334/// # use momento_functions_host::cache;
335/// # use momento_functions_host::cache::{SetIfHashCondition, SetIfHashResult};
336/// # use std::time::Duration;
337/// // First, get the current value and its hash
338/// // let entry = cache::get_with_hash("my_key")?;
339/// let previous_hash = vec![1, 2, 3]; // Hash from a previous get_with_hash call
340///
341/// match cache::set_if_hash(
342/// "my_key",
343/// b"new_value".to_vec(),
344/// Duration::from_secs(60),
345/// SetIfHashCondition::PresentAndHashEqual(previous_hash),
346/// ) {
347/// Ok(SetIfHashResult::Stored(new_hash)) => {
348/// log::info!("Value updated, new hash: {:?}", new_hash);
349/// }
350/// Ok(SetIfHashResult::NotStored) => {
351/// log::info!("Value was modified by another process");
352/// }
353/// Err(e) => eprintln!("set_if_hash failed: {e}"),
354/// }
355/// ```
356pub fn set_if_hash<E: Encode>(
357 key: impl AsRef<[u8]>,
358 value: E,
359 ttl: Duration,
360 condition: SetIfHashCondition,
361) -> Result<SetIfHashResult, CacheSetIfHashError<E::Error>> {
362 cache_scalar::set_if_hash(
363 key.as_ref(),
364 &value
365 .try_serialize()
366 .map_err(|e| CacheSetIfHashError::EncodeFailed { cause: e })?
367 .into(),
368 saturate_ttl(ttl),
369 &condition,
370 )
371 .map_err(Into::into)
372}
373
374/// Represents the desired behavior for managing the TTL on collections.
375///
376/// The first time the collection is created, it needs to set a TTL. For subsequent operations
377/// that modify the collection, you may choose to update the TTL in order to prolong the life
378/// of the cached collection, or to leave the TTL unmodified to ensure the collection expires
379/// at the original TTL.
380#[derive(Copy, Clone, Debug, PartialEq, Eq)]
381pub struct CollectionTtl {
382 ttl: Duration,
383 refresh: bool,
384}
385
386impl CollectionTtl {
387 /// Create a collection TTL with the provided `ttl` and `refresh` settings.
388 pub const fn new(ttl: Duration, refresh: bool) -> Self {
389 Self { ttl, refresh }
390 }
391
392 /// Create a collection TTL that updates the TTL for the collection any time it is
393 /// modified.
394 pub fn refresh_on_update(ttl: impl Into<Duration>) -> Self {
395 Self::new(ttl.into(), true)
396 }
397
398 /// Create a collection TTL that will not refresh the TTL for the collection when
399 /// it is updated.
400 ///
401 /// Use this if you want to be sure that the collection expires at the originally
402 /// specified time, even if you make modifications to the value of the collection.
403 ///
404 /// The TTL will still be used when a new collection is created.
405 pub fn initialize_only(ttl: impl Into<Duration>) -> Self {
406 Self::new(ttl.into(), false)
407 }
408
409 /// Return a new collection TTL which uses the same TTL but refreshes on updates.
410 pub fn with_refresh_on_update(self) -> Self {
411 Self::new(self.ttl(), true)
412 }
413
414 /// Return a new collection TTL which uses the same TTL but does not refresh on
415 /// updates.
416 pub fn with_no_refresh_on_update(self) -> Self {
417 Self::new(self.ttl(), false)
418 }
419
420 /// Return a new collection TTL which has the same refresh behavior but uses the
421 /// provided TTL.
422 pub fn with_ttl(self, ttl: impl Into<Duration>) -> Self {
423 Self::new(ttl.into(), self.refresh())
424 }
425
426 /// Constructs a CollectionTtl with the specified TTL. The TTL for the collection will be
427 /// refreshed any time the collection is modified.
428 pub fn of(ttl: Duration) -> Self {
429 Self::new(ttl, true)
430 }
431
432 /// The [`Duration`] after which the cached collection should be expired from the
433 /// cache.
434 pub fn ttl(&self) -> Duration {
435 self.ttl
436 }
437
438 /// Whether the collection's TTL will be refreshed on every update.
439 ///
440 /// If true, this will extend the time at which the collection would expire when
441 /// an update operation happens. Otherwise, the collection's TTL will only be set
442 /// when it is initially created.
443 pub fn refresh(&self) -> bool {
444 self.refresh
445 }
446}
447
448/// An error occurred when pushing a value to the back of a list in the cache.
449#[derive(thiserror::Error, Debug)]
450pub enum CacheListPushBackError<E: EncodeError> {
451 /// The provided value could not be encoded.
452 #[error("Failed to encode value.")]
453 EncodeFailed {
454 /// The underlying encoding error.
455 cause: E,
456 },
457 /// An error occurred when calling the host cache function.
458 #[error(transparent)]
459 CacheError(#[from] cache_list::Error),
460}
461
462/// An error occurred when pushing a value to the front of a list in the cache.
463#[derive(thiserror::Error, Debug)]
464pub enum CacheListPushFrontError<E: EncodeError> {
465 /// The provided value could not be encoded.
466 #[error("Failed to encode value.")]
467 EncodeFailed {
468 /// The underlying encoding error.
469 cause: E,
470 },
471 /// An error occurred when calling the host cache function.
472 #[error(transparent)]
473 CacheError(#[from] cache_list::Error),
474}
475
476/// Adds an element to the back of the given list. Creates the list if it does not already exist.
477///
478/// # Arguments
479/// * `list_name` - The name of the list.
480/// * `value` - The value to append to the list.
481/// * `collection_ttl` - The time-to-live for the list.
482/// * `truncate_front_to_size` - If the list exceeds this length, remove excess from the front of the list.
483///
484/// # Examples:
485/// ________
486/// Append a value to the back of a list:
487/// ```rust,no_run
488/// # use momento_functions_host::cache;
489/// # use momento_functions_host::cache::CollectionTtl;
490/// # use std::time::Duration;
491/// match cache::list_push_back(
492/// "my_list",
493/// b"new_value".to_vec(),
494/// CollectionTtl::of(Duration::from_secs(60)),
495/// None,
496/// ) {
497/// Ok(list_length) => log::info!("New length of my_list: {}", list_length),
498/// Err(e) => eprintln!("list_push_back failed: {e}"),
499/// }
500/// ```
501pub fn list_push_back<E: Encode>(
502 list_name: impl AsRef<[u8]>,
503 value: E,
504 collection_ttl: CollectionTtl,
505 truncate_front_to_size: Option<u32>,
506) -> Result<u32, CacheListPushBackError<E::Error>> {
507 cache_list::list_push_back(
508 list_name.as_ref(),
509 &value
510 .try_serialize()
511 .map_err(|e| CacheListPushBackError::EncodeFailed { cause: e })?
512 .into(),
513 saturate_ttl(collection_ttl.ttl()),
514 collection_ttl.refresh(),
515 truncate_front_to_size.unwrap_or(0),
516 )
517 .map_err(Into::into)
518}
519
520/// Adds an element to the front of the given list. Creates the list if it does not already exist.
521///
522/// # Arguments
523/// * `list_name` - The name of the list.
524/// * `value` - The value to append to the list.
525/// * `collection_ttl` - The time-to-live for the list.
526/// * `truncate_back_to_size` - If the list exceeds this length, remove excess from the back of the list.
527///
528/// # Examples:
529/// ________
530/// Append a value to the back of a list:
531/// ```rust,no_run
532/// # use momento_functions_host::cache;
533/// # use momento_functions_host::cache::CollectionTtl;
534/// # use std::time::Duration;
535/// match cache::list_push_front(
536/// "my_list",
537/// b"new_value".to_vec(),
538/// CollectionTtl::of(Duration::from_secs(60)),
539/// None,
540/// ) {
541/// Ok(list_length) => log::info!("New length of my_list: {}", list_length),
542/// Err(e) => eprintln!("list_push_front failed: {e}"),
543/// }
544/// ```
545pub fn list_push_front<E: Encode>(
546 list_name: impl AsRef<[u8]>,
547 value: E,
548 collection_ttl: CollectionTtl,
549 truncate_back_to_size: Option<u32>,
550) -> Result<u32, CacheListPushFrontError<E::Error>> {
551 cache_list::list_push_front(
552 list_name.as_ref(),
553 &value
554 .try_serialize()
555 .map_err(|e| CacheListPushFrontError::EncodeFailed { cause: e })?
556 .into(),
557 saturate_ttl(collection_ttl.ttl()),
558 collection_ttl.refresh(),
559 truncate_back_to_size.unwrap_or(0),
560 )
561 .map_err(Into::into)
562}
563
564/// The start bound for a list fetch or retain operation.
565pub enum StartIndex {
566 /// Start from the beginning of the list.
567 Unbounded,
568 /// Start from the given index (inclusive). Negative values count from the end.
569 Inclusive(i32),
570}
571
572impl From<StartIndex> for cache_list::StartIndex {
573 fn from(value: StartIndex) -> Self {
574 match value {
575 StartIndex::Unbounded => Self::Unbounded,
576 StartIndex::Inclusive(i) => Self::Inclusive(i),
577 }
578 }
579}
580
581/// The end bound for a list fetch or retain operation.
582pub enum EndIndex {
583 /// Extend to the end of the list.
584 Unbounded,
585 /// End at the given index (exclusive). Negative values count from the end.
586 Exclusive(i32),
587}
588
589impl From<EndIndex> for cache_list::EndIndex {
590 fn from(value: EndIndex) -> Self {
591 match value {
592 EndIndex::Unbounded => Self::Unbounded,
593 EndIndex::Exclusive(i) => Self::Exclusive(i),
594 }
595 }
596}
597
598/// An error occurred when fetching a list from the cache.
599#[derive(thiserror::Error, Debug)]
600pub enum CacheListFetchError<E: ExtractError> {
601 /// The value could not be extracted with the provided implementation.
602 #[error("Failed to extract value.")]
603 ExtractFailed {
604 /// The underlying error.
605 cause: E,
606 },
607 /// An error occurred when calling the host cache function.
608 #[error(transparent)]
609 CacheError(#[from] cache_list::Error),
610}
611
612/// Gets a list from the cache with optional slices.
613///
614/// # Arguments
615/// * `list_name` - The name of the list.
616/// * `start_index` - The starting inclusive element of the list to fetch. Default is 0.
617/// * `end_index` - The ending exclusive element of the list to fetch. Default is up to and including end of list.
618///
619/// # Examples
620/// ________
621/// Bytes:
622/// ```rust,no_run
623/// # use momento_functions_host::cache;
624/// # use momento_functions_host::cache::{StartIndex, EndIndex};
625/// match cache::list_fetch::<Vec<u8>>("my_list", StartIndex::Unbounded, EndIndex::Unbounded) {
626/// Ok(Some(iter)) => {
627/// match iter.collect::<Result<Vec<Vec<u8>>, _>>() {
628/// Ok(values) => { /* use values */ }
629/// Err(e) => eprintln!("extract failed: {e}"),
630/// }
631/// }
632/// Ok(None) => { /* list not found */ }
633/// Err(e) => eprintln!("list_fetch failed: {e}"),
634/// }
635/// ```
636/// ________
637/// Json:
638/// ```rust,no_run
639/// # use momento_functions_host::cache;
640/// # use momento_functions_host::cache::{StartIndex, EndIndex};
641/// use momento_functions_host::encoding::Json;
642///
643/// #[derive(serde::Deserialize)]
644/// struct MyStruct {
645/// message: String,
646/// }
647///
648/// match cache::list_fetch::<Json<MyStruct>>("my_list", StartIndex::Unbounded, EndIndex::Unbounded) {
649/// Ok(Some(iter)) => {
650/// match iter.collect::<Result<Vec<Json<MyStruct>>, _>>() {
651/// Ok(values) => { /* use values */ }
652/// Err(e) => eprintln!("extract failed: {e}"),
653/// }
654/// }
655/// Ok(None) => { /* list not found */ }
656/// Err(e) => eprintln!("list_fetch failed: {e}"),
657/// }
658/// ```
659#[allow(clippy::type_complexity)]
660pub fn list_fetch<T: Extract>(
661 list_name: impl AsRef<[u8]>,
662 start_index: StartIndex,
663 end_index: EndIndex,
664) -> Result<
665 Option<impl Iterator<Item = Result<T, CacheListFetchError<T::Error>>>>,
666 CacheListFetchError<T::Error>,
667> {
668 Ok(
669 match cache_list::list_fetch(list_name.as_ref(), start_index.into(), end_index.into())? {
670 FetchResponse::Found(items) => Some(items.into_iter().map(|item| {
671 T::extract(item).map_err(|e| CacheListFetchError::ExtractFailed { cause: e })
672 })),
673 FetchResponse::Missing => None,
674 },
675 )
676}