Skip to main content

matrix_sdk_indexeddb/transaction/
mod.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License
14
15// Allow dead code here, as this module is still in the process
16// of being developed, so some functions will be used later on.
17// Once development is complete, we can remove this line and
18// clean up any dead code.
19#![allow(dead_code)]
20
21use futures_util::TryStreamExt;
22use indexed_db_futures::{
23    BuildSerde, cursor::CursorDirection, internals::SystemRepr, query_source::QuerySource,
24    transaction as inner,
25};
26use serde::{
27    Serialize,
28    de::{DeserializeOwned, Error},
29};
30use thiserror::Error;
31use wasm_bindgen::JsValue;
32
33use crate::{
34    error::{AsyncErrorDeps, GenericError},
35    serializer::indexed_type::{
36        IndexedTypeSerializer,
37        range::IndexedKeyRange,
38        traits::{Indexed, IndexedKey},
39    },
40};
41
42#[derive(Debug, Error)]
43pub enum TransactionError {
44    #[error("DomException {name} ({code}): {message}")]
45    DomException { name: String, message: String, code: u16 },
46    #[error("serialization: {0}")]
47    Serialization(Box<dyn AsyncErrorDeps>),
48    #[error("item is not unique")]
49    ItemIsNotUnique,
50    #[error("item not found")]
51    ItemNotFound,
52    #[error("a numerical operation overflowed")]
53    NumericalOverflow,
54    #[error("backend: {0}")]
55    Backend(Box<dyn AsyncErrorDeps>),
56}
57
58impl From<web_sys::DomException> for TransactionError {
59    fn from(value: web_sys::DomException) -> Self {
60        Self::DomException { name: value.name(), message: value.message(), code: value.code() }
61    }
62}
63
64impl From<serde_wasm_bindgen::Error> for TransactionError {
65    fn from(e: serde_wasm_bindgen::Error) -> Self {
66        Self::Serialization(Box::new(serde_json::Error::custom(e.to_string())))
67    }
68}
69
70impl From<indexed_db_futures::error::SerialisationError> for TransactionError {
71    fn from(e: indexed_db_futures::error::SerialisationError) -> Self {
72        Self::Serialization(Box::new(serde_json::Error::custom(e.to_string())))
73    }
74}
75
76impl From<indexed_db_futures::error::JSError> for TransactionError {
77    fn from(value: indexed_db_futures::error::JSError) -> Self {
78        Self::Backend(Box::new(GenericError::from(value.to_string())))
79    }
80}
81
82impl From<indexed_db_futures::error::Error> for TransactionError {
83    fn from(value: indexed_db_futures::error::Error) -> Self {
84        use indexed_db_futures::error::Error;
85        match value {
86            Error::DomException(e) => e.into_sys().into(),
87            Error::Serialisation(e) => e.into(),
88            Error::MissingData(e) => Self::Backend(Box::new(e)),
89            Error::Unknown(e) => e.into(),
90        }
91    }
92}
93
94/// Represents an IndexedDB transaction, but provides a convenient interface for
95/// performing operations on types that implement [`Indexed`] and related
96/// traits.
97pub struct Transaction<'a> {
98    transaction: inner::Transaction<'a>,
99    serializer: &'a IndexedTypeSerializer,
100}
101
102impl<'a> Transaction<'a> {
103    pub fn new(transaction: inner::Transaction<'a>, serializer: &'a IndexedTypeSerializer) -> Self {
104        Self { transaction, serializer }
105    }
106
107    /// Returns the serializer performing (de)serialization for this
108    /// [`Transaction`]
109    pub fn serializer(&self) -> &IndexedTypeSerializer {
110        self.serializer
111    }
112
113    /// Returns the underlying IndexedDB transaction.
114    pub fn into_inner(self) -> inner::Transaction<'a> {
115        self.transaction
116    }
117
118    /// Commit all operations tracked in this transaction to IndexedDB.
119    pub async fn commit(self) -> Result<(), TransactionError> {
120        self.transaction.commit().await.map_err(Into::into)
121    }
122
123    /// Query IndexedDB for items that match the given key range
124    pub async fn get_items_by_key<T, K>(
125        &self,
126        range: impl Into<IndexedKeyRange<K>>,
127    ) -> Result<Vec<T>, TransactionError>
128    where
129        T: Indexed,
130        T::IndexedType: DeserializeOwned,
131        T::Error: AsyncErrorDeps,
132        K: IndexedKey<T> + Serialize,
133    {
134        let range = self.serializer.encode_key_range::<T, K>(range);
135        let object_store = self.transaction.object_store(T::OBJECT_STORE)?;
136        let array = if let Some(index) = K::INDEX {
137            object_store.index(index)?.get_all().with_query(range).serde()?.await?
138        } else {
139            object_store.get_all().with_query(range).serde()?.await?
140        };
141        let mut items = Vec::with_capacity(array.len());
142        for value in array {
143            let item = T::from_indexed(value?, self.serializer.inner())
144                .map_err(|e| TransactionError::Serialization(Box::new(e)))?;
145            items.push(item);
146        }
147        Ok(items)
148    }
149
150    /// Query IndexedDB for items that match the given key component range
151    pub async fn get_items_by_key_components<'b, T, K>(
152        &self,
153        range: impl Into<IndexedKeyRange<K::KeyComponents<'b>>>,
154    ) -> Result<Vec<T>, TransactionError>
155    where
156        T: Indexed + 'b,
157        T::IndexedType: DeserializeOwned,
158        T::Error: AsyncErrorDeps,
159        K: IndexedKey<T> + Serialize + 'b,
160    {
161        let range: IndexedKeyRange<K> = range.into().encoded(self.serializer.inner());
162        self.get_items_by_key::<T, K>(range).await
163    }
164
165    /// Query IndexedDB for items that match the given key. If
166    /// more than one item is found, an error is returned.
167    pub async fn get_item_by_key<T, K>(&self, key: K) -> Result<Option<T>, TransactionError>
168    where
169        T: Indexed,
170        T::IndexedType: DeserializeOwned,
171        T::Error: AsyncErrorDeps,
172        K: IndexedKey<T> + Serialize,
173    {
174        let mut items = self.get_items_by_key::<T, K>(key).await?;
175        if items.len() > 1 {
176            return Err(TransactionError::ItemIsNotUnique);
177        }
178        Ok(items.pop())
179    }
180
181    /// Query IndexedDB for items that match the given key components. If more
182    /// than one item is found, an error is returned.
183    pub async fn get_item_by_key_components<'b, T, K>(
184        &self,
185        components: K::KeyComponents<'b>,
186    ) -> Result<Option<T>, TransactionError>
187    where
188        T: Indexed + 'b,
189        T::IndexedType: DeserializeOwned,
190        T::Error: AsyncErrorDeps,
191        K: IndexedKey<T> + Serialize + 'b,
192    {
193        let mut items = self.get_items_by_key_components::<T, K>(components).await?;
194        if items.len() > 1 {
195            return Err(TransactionError::ItemIsNotUnique);
196        }
197        Ok(items.pop())
198    }
199
200    /// Query IndexedDB for the number of items that match the given key range.
201    pub async fn get_items_count_by_key<T, K>(
202        &self,
203        range: impl Into<IndexedKeyRange<K>>,
204    ) -> Result<usize, TransactionError>
205    where
206        T: Indexed,
207        T::IndexedType: DeserializeOwned,
208        T::Error: AsyncErrorDeps,
209        K: IndexedKey<T> + Serialize,
210    {
211        let range = self.serializer.encode_key_range::<T, K>(range);
212        let object_store = self.transaction.object_store(T::OBJECT_STORE)?;
213        let count = if let Some(index) = K::INDEX {
214            object_store.index(index)?.count().with_query(range).serde()?.await?
215        } else {
216            object_store.count().with_query(range).serde()?.await?
217        };
218        Ok(count as usize)
219    }
220
221    /// Query IndexedDB for the number of items that match the given key
222    /// components range.
223    pub async fn get_items_count_by_key_components<'b, T, K>(
224        &self,
225        range: impl Into<IndexedKeyRange<K::KeyComponents<'b>>>,
226    ) -> Result<usize, TransactionError>
227    where
228        T: Indexed + 'b,
229        T::IndexedType: DeserializeOwned,
230        T::Error: AsyncErrorDeps,
231        K: IndexedKey<T> + Serialize + 'b,
232    {
233        let range: IndexedKeyRange<K> = range.into().encoded(self.serializer.inner());
234        self.get_items_count_by_key::<T, K>(range).await
235    }
236
237    /// Query IndexedDB for the item with the maximum key in the given range.
238    pub async fn get_max_item_by_key<T, K>(
239        &self,
240        range: impl Into<IndexedKeyRange<K>>,
241    ) -> Result<Option<T>, TransactionError>
242    where
243        T: Indexed,
244        T::IndexedType: DeserializeOwned,
245        T::Error: AsyncErrorDeps,
246        K: IndexedKey<T> + Serialize + DeserializeOwned,
247    {
248        if let Some(key) = self.get_max_key::<T, K>(range).await? {
249            return self.get_item_by_key::<T, K>(key).await;
250        }
251        Ok(None)
252    }
253
254    /// Query IndexedDB for keys that match the given key range.
255    pub async fn get_keys<T, K>(
256        &self,
257        range: impl Into<IndexedKeyRange<K>>,
258    ) -> Result<Vec<K>, TransactionError>
259    where
260        T: Indexed,
261        K: IndexedKey<T> + Serialize + DeserializeOwned,
262    {
263        let range = self.serializer.encode_key_range::<T, K>(range);
264        let object_store = self.transaction.object_store(T::OBJECT_STORE)?;
265        if let Some(index) = K::INDEX {
266            let index = object_store.index(index)?;
267            if let Some(cursor) = index.open_key_cursor().with_query(range).serde()?.await? {
268                return cursor.key_stream_ser().try_collect().await.map_err(Into::into);
269            }
270        } else if let Some(cursor) =
271            object_store.open_key_cursor().with_query(range).serde()?.await?
272        {
273            return cursor.key_stream_ser().try_collect().await.map_err(Into::into);
274        }
275        Ok(Vec::new())
276    }
277
278    /// Query IndexedDB for the maximum key in the given range.
279    pub async fn get_max_key<T, K>(
280        &self,
281        range: impl Into<IndexedKeyRange<K>>,
282    ) -> Result<Option<K>, TransactionError>
283    where
284        T: Indexed,
285        K: IndexedKey<T> + Serialize + DeserializeOwned,
286    {
287        let range = self.serializer.encode_key_range::<T, K>(range);
288        let direction = CursorDirection::Prev;
289        let object_store = self.transaction.object_store(T::OBJECT_STORE)?;
290        if let Some(index) = K::INDEX {
291            let index = object_store.index(index)?;
292            if let Some(mut cursor) =
293                index.open_key_cursor().with_query(range).with_direction(direction).serde()?.await?
294            {
295                return cursor.next_key_ser().await.map_err(Into::into);
296            }
297        } else if let Some(mut cursor) = object_store
298            .open_key_cursor()
299            .with_query(range)
300            .with_direction(direction)
301            .serde()?
302            .await?
303        {
304            return cursor.next_key_ser().await.map_err(Into::into);
305        }
306        Ok(None)
307    }
308
309    /// Query IndexedDB for keys that match the given key range. Iterate over
310    /// the keys in the given [`direction`](CursorDirection) using a cursor and
311    /// fold them into an accumulator while the given function `f` returns
312    /// [`Some`].
313    ///
314    /// This function returns the final value of the accumulator and the key, if
315    /// any, which caused the fold to short circuit.
316    ///
317    /// Note that the use of cursor means that keys are read lazily from
318    /// IndexedDB.
319    pub async fn fold_keys_while<T, K, Acc, F>(
320        &self,
321        direction: CursorDirection,
322        range: impl Into<IndexedKeyRange<K>>,
323        init: Acc,
324        mut f: F,
325    ) -> Result<(Acc, Option<K>), TransactionError>
326    where
327        T: Indexed,
328        K: IndexedKey<T> + Serialize + DeserializeOwned,
329        F: FnMut(&Acc, &K) -> Option<Acc>,
330    {
331        let range = self.serializer.encode_key_range::<T, K>(range);
332        let object_store = self.transaction.object_store(T::OBJECT_STORE)?;
333
334        let mut state = init;
335        if let Some(index) = K::INDEX {
336            let index = object_store.index(index)?;
337            if let Some(mut cursor) =
338                index.open_key_cursor().with_query(range).with_direction(direction).serde()?.await?
339            {
340                while let Some(key) = cursor.next_key_ser().await? {
341                    match f(&state, &key) {
342                        Some(s) => state = s,
343                        None => return Ok((state, Some(key))),
344                    }
345                }
346            }
347        } else if let Some(mut cursor) = object_store
348            .open_key_cursor()
349            .with_query(range)
350            .with_direction(direction)
351            .serde()?
352            .await?
353        {
354            while let Some(key) = cursor.next_key_ser().await? {
355                match f(&state, &key) {
356                    Some(s) => state = s,
357                    None => return Ok((state, Some(key))),
358                }
359            }
360        }
361        Ok((state, None))
362    }
363
364    /// Adds an item to the corresponding IndexedDB object
365    /// store, i.e., `T::OBJECT_STORE`. If an item with the same key already
366    /// exists, it will be rejected. When the item is successfully added, the
367    /// function returns the intermediary type [`Indexed::IndexedType`] in case
368    /// inspection is needed.
369    pub async fn add_item<T>(&self, item: &T) -> Result<T::IndexedType, TransactionError>
370    where
371        T: Indexed + Serialize,
372        T::IndexedType: Serialize,
373        T::Error: AsyncErrorDeps,
374    {
375        let output = self
376            .serializer
377            .serialize(item)
378            .map_err(|e| TransactionError::Serialization(Box::new(e)))?;
379        self.transaction.object_store(T::OBJECT_STORE)?.add(output.value).await?;
380        Ok(output.indexed)
381    }
382
383    /// Puts an item in the corresponding IndexedDB object
384    /// store, i.e., `T::OBJECT_STORE`. If an item with the same key already
385    /// exists, it will be overwritten. When the item is successfully put, the
386    /// function returns the intermediary type [`Indexed::IndexedType`] in case
387    /// inspection is needed.
388    pub async fn put_item<T>(&self, item: &T) -> Result<T::IndexedType, TransactionError>
389    where
390        T: Indexed + Serialize,
391        T::IndexedType: Serialize,
392        T::Error: AsyncErrorDeps,
393    {
394        let output = self
395            .serializer
396            .serialize(item)
397            .map_err(|e| TransactionError::Serialization(Box::new(e)))?;
398        self.transaction.object_store(T::OBJECT_STORE)?.put(output.value).await?;
399        Ok(output.indexed)
400    }
401
402    /// Puts an item in the corresponding IndexedDB object
403    /// store, i.e., `T::OBJECT_STORE`, if `T::IndexedType` meets the criteria
404    /// defined by `f`. If an item with the same key already
405    /// exists, it will be overwritten. When the item is successfully put, the
406    /// function returns the intermediary type [`Indexed::IndexedType`] in case
407    /// inspection is needed.
408    pub async fn put_item_if<T>(
409        &self,
410        item: &T,
411        f: impl Fn(&T::IndexedType) -> bool,
412    ) -> Result<Option<T::IndexedType>, TransactionError>
413    where
414        T: Indexed + Serialize,
415        T::IndexedType: Serialize,
416        T::Error: AsyncErrorDeps,
417    {
418        let option = self
419            .serializer
420            .serialize_if(item, f)
421            .map_err(|e| TransactionError::Serialization(Box::new(e)))?;
422        if let Some(output) = option {
423            self.transaction.object_store(T::OBJECT_STORE)?.put(output.value).await?;
424            Ok(Some(output.indexed))
425        } else {
426            Ok(None)
427        }
428    }
429
430    /// Delete items in given key range from IndexedDB
431    pub async fn delete_items_by_key<T, K>(
432        &self,
433        range: impl Into<IndexedKeyRange<K>>,
434    ) -> Result<(), TransactionError>
435    where
436        T: Indexed,
437        K: IndexedKey<T> + Serialize,
438    {
439        let range = self.serializer.encode_key_range::<T, K>(range);
440        let object_store = self.transaction.object_store(T::OBJECT_STORE)?;
441        if let Some(index) = K::INDEX {
442            let index = object_store.index(index)?;
443            if let Some(mut cursor) = index.open_cursor().with_query(range).serde()?.await? {
444                loop {
445                    cursor.delete()?;
446                    if cursor.next_record::<JsValue>().await?.is_none() {
447                        break;
448                    }
449                }
450            }
451        } else {
452            object_store.delete(range).serde()?.await?;
453        }
454        Ok(())
455    }
456
457    /// Delete items in the given key component range from
458    /// IndexedDB
459    pub async fn delete_items_by_key_components<'b, T, K>(
460        &self,
461        range: impl Into<IndexedKeyRange<K::KeyComponents<'b>>>,
462    ) -> Result<(), TransactionError>
463    where
464        T: Indexed + 'b,
465        K: IndexedKey<T> + Serialize + 'b,
466    {
467        let range: IndexedKeyRange<K> = range.into().encoded(self.serializer.inner());
468        self.delete_items_by_key::<T, K>(range).await
469    }
470
471    /// Delete item that matches the given key components from
472    /// IndexedDB
473    pub async fn delete_item_by_key<'b, T, K>(
474        &self,
475        key: K::KeyComponents<'b>,
476    ) -> Result<(), TransactionError>
477    where
478        T: Indexed + 'b,
479        K: IndexedKey<T> + Serialize + 'b,
480    {
481        self.delete_items_by_key_components::<T, K>(key).await
482    }
483
484    /// Clear all items of type `T` from the associated object store
485    /// `T::OBJECT_STORE` from IndexedDB
486    pub async fn clear<T>(&self) -> Result<(), TransactionError>
487    where
488        T: Indexed,
489    {
490        self.transaction.object_store(T::OBJECT_STORE)?.clear()?.await.map_err(Into::into)
491    }
492}