Skip to main content

storage_traits/
indexeddb.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cmp::{Ordering, PartialEq, PartialOrd};
6use std::collections::{HashMap, HashSet};
7use std::error::Error;
8use std::fmt::{Debug, Display, Formatter};
9
10use malloc_size_of_derive::MallocSizeOf;
11use profile_traits::generic_callback::GenericCallback;
12use profile_traits::mem::ReportsChan;
13use serde::{Deserialize, Serialize};
14use servo_base::generic_channel::GenericSender;
15use servo_url::origin::ImmutableOrigin;
16use uuid::Uuid;
17
18use crate::client_storage::StorageProxyMap;
19
20// TODO Box<dyn Error> is not serializable, fix needs to be found
21pub type DbError = String;
22/// A DbResult wraps any part of a call that has to reach into the backend (in this case sqlite.rs)
23/// These errors could be anything, depending on the backend
24pub type DbResult<T> = Result<T, DbError>;
25
26/// Any error from the backend, a super-set of [`DbError`]
27#[derive(Clone, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)]
28pub enum BackendError {
29    /// The requested database does not exist
30    DbNotFound,
31    /// The requested object store does not exist
32    StoreNotFound,
33    /// The storage quota was exceeded
34    QuotaExceeded,
35    /// The transaction was aborted
36    Abort,
37
38    DbErr(DbError),
39}
40
41impl From<DbError> for BackendError {
42    fn from(value: DbError) -> Self {
43        BackendError::DbErr(value)
44    }
45}
46
47impl Display for BackendError {
48    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
49        match self {
50            BackendError::DbNotFound => write!(f, "DbNotFound"),
51            BackendError::StoreNotFound => write!(f, "StoreNotFound"),
52            BackendError::QuotaExceeded => write!(f, "QuotaExceeded"),
53            BackendError::Abort => write!(f, "Abort"),
54            BackendError::DbErr(err) => write!(f, "{err}"),
55        }
56    }
57}
58
59impl Error for BackendError {}
60
61pub type BackendResult<T> = Result<T, BackendError>;
62
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, MallocSizeOf, Serialize)]
64pub enum KeyPath {
65    String(String),
66    Sequence(Vec<String>),
67}
68
69// https://www.w3.org/TR/IndexedDB-3/#enumdef-idbtransactionmode
70#[derive(Clone, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)]
71pub enum IndexedDBTxnMode {
72    Readonly,
73    Readwrite,
74    Versionchange,
75}
76
77/// <https://www.w3.org/TR/IndexedDB-3/#key-type>
78#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
79pub enum IndexedDBKeyType {
80    Number(f64),
81    String(String),
82    Binary(Vec<u8>),
83    Date(f64),
84    Array(Vec<IndexedDBKeyType>),
85    // FIXME:(arihant2math) implment ArrayBuffer
86}
87
88/// <https://www.w3.org/TR/IndexedDB-3/#compare-two-keys>
89impl PartialOrd for IndexedDBKeyType {
90    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
91        // 1. Let ta be the type of a.
92        // 2. Let tb be the type of b.
93
94        match (self, other) {
95            // Step 3: If ta is array and tb is binary, string, date or number, return 1.
96            (
97                IndexedDBKeyType::Array(_),
98                IndexedDBKeyType::Binary(_) |
99                IndexedDBKeyType::Date(_) |
100                IndexedDBKeyType::Number(_) |
101                IndexedDBKeyType::String(_),
102            ) => Some(Ordering::Greater),
103            // Step 4: If tb is array and ta is binary, string, date or number, return -1.
104            (
105                IndexedDBKeyType::Binary(_) |
106                IndexedDBKeyType::Date(_) |
107                IndexedDBKeyType::Number(_) |
108                IndexedDBKeyType::String(_),
109                IndexedDBKeyType::Array(_),
110            ) => Some(Ordering::Less),
111            // Step 5: If ta is binary and tb is string, date or number, return 1.
112            (
113                IndexedDBKeyType::Binary(_),
114                IndexedDBKeyType::String(_) |
115                IndexedDBKeyType::Date(_) |
116                IndexedDBKeyType::Number(_),
117            ) => Some(Ordering::Greater),
118            // Step 6: If tb is binary and ta is string, date or number, return -1.
119            (
120                IndexedDBKeyType::String(_) |
121                IndexedDBKeyType::Date(_) |
122                IndexedDBKeyType::Number(_),
123                IndexedDBKeyType::Binary(_),
124            ) => Some(Ordering::Less),
125            // Step 7: If ta is string and tb is date or number, return 1.
126            (
127                IndexedDBKeyType::String(_),
128                IndexedDBKeyType::Date(_) | IndexedDBKeyType::Number(_),
129            ) => Some(Ordering::Greater),
130            // Step 8: If tb is string and ta is date or number, return -1.
131            (
132                IndexedDBKeyType::Date(_) | IndexedDBKeyType::Number(_),
133                IndexedDBKeyType::String(_),
134            ) => Some(Ordering::Less),
135            // Step 9: If ta is date and tb is number, return 1.
136            (IndexedDBKeyType::Date(_), IndexedDBKeyType::Number(_)) => Some(Ordering::Greater),
137            // Step 10: If tb is date and ta is number, return -1.
138            (IndexedDBKeyType::Number(_), IndexedDBKeyType::Date(_)) => Some(Ordering::Less),
139            // Step 11 skipped
140            // TODO: Likely a tiny bit wrong (use js number comparison)
141            (IndexedDBKeyType::Number(a), IndexedDBKeyType::Number(b)) => a.partial_cmp(b),
142            // TODO: Likely a tiny bit wrong (use js string comparison)
143            (IndexedDBKeyType::String(a), IndexedDBKeyType::String(b)) => a.partial_cmp(b),
144            // TODO: Likely a little wrong (use js binary comparison)
145            (IndexedDBKeyType::Binary(a), IndexedDBKeyType::Binary(b)) => a.partial_cmp(b),
146            (IndexedDBKeyType::Date(a), IndexedDBKeyType::Date(b)) => a.partial_cmp(b),
147            // TODO: Probably also wrong (the items in a and b should be compared, double check against the spec)
148            (IndexedDBKeyType::Array(a), IndexedDBKeyType::Array(b)) => a.partial_cmp(b),
149            // No catch-all is used, rust ensures that all variants are handled
150        }
151    }
152}
153
154impl PartialEq for IndexedDBKeyType {
155    fn eq(&self, other: &Self) -> bool {
156        let cmp = self.partial_cmp(other);
157        match cmp {
158            Some(Ordering::Equal) => true,
159            Some(Ordering::Less) | Some(Ordering::Greater) => false,
160            None => {
161                // If we can't compare the two keys, we assume they are not equal.
162                false
163            },
164        }
165    }
166}
167
168// <https://www.w3.org/TR/IndexedDB-3/#key-range>
169#[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, Serialize)]
170pub struct IndexedDBKeyRange {
171    pub lower: Option<IndexedDBKeyType>,
172    pub upper: Option<IndexedDBKeyType>,
173    pub lower_open: bool,
174    pub upper_open: bool,
175}
176
177impl From<IndexedDBKeyType> for IndexedDBKeyRange {
178    fn from(key: IndexedDBKeyType) -> Self {
179        IndexedDBKeyRange {
180            lower: Some(key.clone()),
181            upper: Some(key),
182            ..Default::default()
183        }
184    }
185}
186
187impl IndexedDBKeyRange {
188    pub fn only(key: IndexedDBKeyType) -> Self {
189        Self::from(key)
190    }
191
192    pub fn new(
193        lower: Option<IndexedDBKeyType>,
194        upper: Option<IndexedDBKeyType>,
195        lower_open: bool,
196        upper_open: bool,
197    ) -> Self {
198        IndexedDBKeyRange {
199            lower,
200            upper,
201            lower_open,
202            upper_open,
203        }
204    }
205
206    pub fn lower_bound(key: IndexedDBKeyType, open: bool) -> Self {
207        IndexedDBKeyRange {
208            lower: Some(key),
209            upper: None,
210            lower_open: open,
211            upper_open: true,
212        }
213    }
214
215    pub fn upper_bound(key: IndexedDBKeyType, open: bool) -> Self {
216        IndexedDBKeyRange {
217            lower: None,
218            upper: Some(key),
219            lower_open: true,
220            upper_open: open,
221        }
222    }
223
224    // <https://www.w3.org/TR/IndexedDB-3/#in>
225    pub fn contains(&self, key: &IndexedDBKeyType) -> bool {
226        // A key is in a key range if both of the following conditions are fulfilled:
227        // The lower bound is null, or it is less than key,
228        // or it is both equal to key and the lower open flag is unset.
229        // The upper bound is null, or it is greater than key,
230        // or it is both equal to key and the upper open flag is unset
231        let lower_bound_condition = self
232            .lower
233            .as_ref()
234            .is_none_or(|lower| lower < key || (!self.lower_open && lower == key));
235        let upper_bound_condition = self
236            .upper
237            .as_ref()
238            .is_none_or(|upper| key < upper || (!self.upper_open && key == upper));
239        lower_bound_condition && upper_bound_condition
240    }
241
242    pub fn is_singleton(&self) -> bool {
243        self.lower.is_some() && self.lower == self.upper && !self.lower_open && !self.upper_open
244    }
245
246    pub fn as_singleton(&self) -> Option<&IndexedDBKeyType> {
247        if self.is_singleton() {
248            return Some(self.lower.as_ref().unwrap());
249        }
250        None
251    }
252}
253
254/// <https://w3c.github.io/IndexedDB/#record-snapshot>
255#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
256pub struct IndexedDBRecord {
257    pub key: IndexedDBKeyType,
258    pub primary_key: IndexedDBKeyType,
259    pub value: Vec<u8>,
260}
261
262#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
263pub struct IndexedDBIndex {
264    pub name: String,
265    pub key_path: KeyPath,
266    pub multi_entry: bool,
267    pub unique: bool,
268}
269
270#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
271pub struct IndexedDBObjectStore {
272    pub name: String,
273    pub key_path: Option<KeyPath>,
274    pub has_key_generator: bool,
275    pub key_generator_current_number: Option<i64>,
276    pub indexes: Vec<IndexedDBIndex>,
277}
278
279#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
280pub enum PutItemResult {
281    Key(IndexedDBKeyType),
282    CannotOverwrite,
283}
284
285#[derive(Debug, Deserialize, MallocSizeOf, Serialize)]
286pub enum AsyncReadOnlyOperation {
287    /// Gets the value associated with the given key in the associated idb data
288    GetKey {
289        callback: GenericCallback<BackendResult<Option<IndexedDBKeyType>>>,
290        key_range: IndexedDBKeyRange,
291    },
292    GetItem {
293        callback: GenericCallback<BackendResult<Option<Vec<u8>>>>,
294        key_range: IndexedDBKeyRange,
295    },
296
297    GetAllKeys {
298        callback: GenericCallback<BackendResult<Vec<IndexedDBKeyType>>>,
299        key_range: IndexedDBKeyRange,
300        count: Option<u32>,
301    },
302    GetAllItems {
303        callback: GenericCallback<BackendResult<Vec<Vec<u8>>>>,
304        key_range: IndexedDBKeyRange,
305        count: Option<u32>,
306    },
307
308    Count {
309        callback: GenericCallback<BackendResult<u64>>,
310        key_range: IndexedDBKeyRange,
311    },
312    Iterate {
313        callback: GenericCallback<BackendResult<Vec<IndexedDBRecord>>>,
314        key_range: IndexedDBKeyRange,
315    },
316}
317
318impl AsyncReadOnlyOperation {
319    fn notify_error(&self, error: BackendError) {
320        let _ = match self {
321            Self::GetKey { callback, .. } => callback.send(Err(error)),
322            Self::GetItem { callback, .. } => callback.send(Err(error)),
323            Self::GetAllKeys { callback, .. } => callback.send(Err(error)),
324            Self::GetAllItems { callback, .. } => callback.send(Err(error)),
325            Self::Count { callback, .. } => callback.send(Err(error)),
326            Self::Iterate { callback, .. } => callback.send(Err(error)),
327        };
328    }
329}
330
331#[derive(Debug, Deserialize, MallocSizeOf, Serialize)]
332pub enum AsyncReadWriteOperation {
333    /// Sets the value of the given key in the associated idb data
334    PutItem {
335        callback: GenericCallback<BackendResult<PutItemResult>>,
336        key: Option<IndexedDBKeyType>,
337        value: Vec<u8>,
338        should_overwrite: bool,
339        /// New object store key generator current number to persist if the put succeeds.
340        key_generator_current_number: Option<i64>,
341    },
342
343    /// Removes the key/value pair for the given key in the associated idb data
344    RemoveItem {
345        callback: GenericCallback<BackendResult<()>>,
346        key_range: IndexedDBKeyRange,
347    },
348    /// Clears all key/value pairs in the associated idb data
349    Clear(GenericCallback<BackendResult<()>>),
350}
351
352impl AsyncReadWriteOperation {
353    fn notify_error(&self, error: BackendError) {
354        let _ = match self {
355            Self::PutItem { callback, .. } => callback.send(Err(error)),
356            Self::RemoveItem { callback, .. } => callback.send(Err(error)),
357            Self::Clear(callback) => callback.send(Err(error)),
358        };
359    }
360}
361
362#[derive(Debug, Deserialize, MallocSizeOf, Serialize)]
363pub enum AsyncSchemaOperation {
364    /// Creates a new index for the database
365    CreateIndex {
366        callback: GenericCallback<BackendError>,
367        index_name: String,
368        key_path: KeyPath,
369        unique: bool,
370        multi_entry: bool,
371    },
372    /// Rename an index
373    RenameIndex {
374        callback: GenericCallback<BackendError>,
375        index_name: String,
376        new_name: String,
377    },
378    /// Delete an index
379    DeleteIndex {
380        callback: GenericCallback<BackendError>,
381        index_name: String,
382    },
383    /// Creates a new store for the database
384    CreateObjectStore {
385        callback: GenericCallback<BackendError>,
386        key_path: Option<KeyPath>,
387        auto_increment: bool,
388    },
389    /// Delete an existing object store in the database
390    DeleteObjectStore {
391        callback: GenericCallback<BackendError>,
392    },
393}
394
395impl AsyncSchemaOperation {
396    pub fn notify_error(&self, error: BackendError) {
397        match self {
398            AsyncSchemaOperation::CreateIndex { .. } |
399            AsyncSchemaOperation::RenameIndex { .. } |
400            AsyncSchemaOperation::DeleteIndex { .. } => {},
401            AsyncSchemaOperation::CreateObjectStore { callback, .. } => {
402                let _ = callback.send(error);
403            },
404            AsyncSchemaOperation::DeleteObjectStore { callback, .. } => {
405                let _ = callback.send(error);
406            },
407        };
408    }
409}
410
411/// Operations that are not executed instantly, but rather added to a
412/// queue that is eventually run.
413#[derive(Debug, Deserialize, MallocSizeOf, Serialize)]
414pub enum AsyncOperation {
415    ReadOnly(AsyncReadOnlyOperation),
416    ReadWrite(AsyncReadWriteOperation),
417    Schema(AsyncSchemaOperation),
418}
419
420impl AsyncOperation {
421    pub fn notify_error(&self, error: BackendError) {
422        match self {
423            Self::ReadOnly(operation) => operation.notify_error(error),
424            Self::ReadWrite(operation) => operation.notify_error(error),
425            Self::Schema(operation) => operation.notify_error(error),
426        }
427    }
428}
429
430#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
431pub enum CreateObjectResult {
432    Created,
433    AlreadyExists,
434}
435
436#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
437/// Messaging used in the context of connection lifecycle management.
438pub enum ConnectionMsg {
439    /// Error if a DB is opened for a version lower than the current db version.
440    VersionError { name: String, id: Uuid },
441    /// Opening a connection was aborted.
442    AbortError { name: String, id: Uuid },
443    /// A newly created connection with a version,
444    /// updgraded or not.
445    Connection {
446        name: String,
447        id: Uuid,
448        version: u64,
449        upgraded: bool,
450        // https://w3c.github.io/IndexedDB/#upgrade-transaction-steps
451        // Step 3. Set transaction’s scope to connection’s object store set.
452        object_store_names: Vec<String>,
453    },
454    /// An upgrade transaction for a version started.
455    Upgrade {
456        name: String,
457        id: Uuid,
458        version: u64,
459        old_version: u64,
460        transaction: u64,
461        // https://w3c.github.io/IndexedDB/#upgrade-transaction-steps
462        // Step 3. Set transaction’s scope to connection’s object store set.
463        object_store_names: Vec<String>,
464    },
465    /// A `versionchange` event should be fired for a connection.
466    VersionChange {
467        /// The id of the connection.
468        id: Uuid,
469        /// The name of the connection.
470        name: String,
471        version: u64,
472        old_version: u64,
473    },
474    /// A `blocked` event should be fired for a connection.
475    Blocked {
476        name: String,
477        id: Uuid,
478        version: u64,
479        old_version: u64,
480    },
481    /// A backend error related to the database occurred.
482    DatabaseError {
483        name: String,
484        id: Uuid,
485        error: BackendError,
486    },
487    /// Ask script to recheck whether a transaction can commit now.
488    TxnMaybeCommit { db_name: String, txn: u64 },
489}
490
491#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
492pub struct TxnCompleteMsg {
493    pub origin: ImmutableOrigin,
494    pub db_name: String,
495    pub txn: u64,
496    pub result: BackendResult<()>,
497}
498
499#[derive(Debug, Deserialize, Serialize)]
500pub struct DatabaseInfo {
501    pub name: String,
502    pub version: u64,
503}
504
505#[derive(Debug, Deserialize, Serialize)]
506pub enum SyncOperation {
507    /// Gets existing databases.
508    GetDatabases(
509        GenericCallback<BackendResult<Vec<DatabaseInfo>>>,
510        ImmutableOrigin,
511    ),
512    /// Upgrades the version of the database
513    UpgradeVersion(
514        /// Sender to send new version as the result of the operation
515        GenericSender<BackendResult<u64>>,
516        ImmutableOrigin,
517        String, // Database
518        u64,    // Serial number for the transaction
519        u64,    // Version to upgrade to
520    ),
521    /// Get object store info
522    GetObjectStore(
523        GenericSender<BackendResult<IndexedDBObjectStore>>,
524        ImmutableOrigin,
525        String, // Database
526        String, // Store
527    ),
528    /// Commits changes of a transaction to the database
529    Commit(
530        GenericCallback<TxnCompleteMsg>,
531        ImmutableOrigin,
532        String, // Database
533        u64,    // Transaction serial number
534    ),
535    /// Aborts a transaction in the backend
536    Abort(
537        GenericCallback<TxnCompleteMsg>,
538        ImmutableOrigin,
539        String, // Database
540        u64,    // Transaction serial number
541    ),
542    /// Upgrade transaction finished after its event was fired in script.
543    UpgradeTransactionFinished {
544        origin: ImmutableOrigin,
545        db_name: String,
546        txn: u64,
547        committed: bool,
548    },
549    /// <https://w3c.github.io/IndexedDB/#transaction-lifetime
550    /// Step 3:
551    /// When each request associated with a transaction is processed,
552    /// a success or error event will be fired. While the event is
553    /// being dispatched, the transaction state is set to active, allowing
554    /// additional requests to be made against the transaction. Once the
555    /// event dispatch is complete, the transaction’s state is set to inactive again.
556    RequestHandled {
557        origin: ImmutableOrigin,
558        db_name: String,
559        txn: u64,
560        request_id: u64,
561    },
562    CreateTransaction {
563        sender: GenericSender<BackendResult<u64>>,
564        origin: ImmutableOrigin,
565        db_name: String,
566        mode: IndexedDBTxnMode,
567        scope: Vec<String>,
568    },
569    /// Request script to recheck transaction commit eligibility.
570    TxnMaybeCommit {
571        origin: ImmutableOrigin,
572        db_name: String,
573        txn: u64,
574    },
575    TransactionFinished {
576        origin: ImmutableOrigin,
577        db_name: String,
578        txn: u64,
579    },
580
581    CloseDatabase(
582        ImmutableOrigin,
583        Uuid,
584        String, // Database
585    ),
586
587    OpenDatabase(
588        // Callback for the result.
589        GenericCallback<ConnectionMsg>,
590        // Origin of the request.
591        ImmutableOrigin,
592        // Name of the database.
593        String,
594        // Requested db version(optional).
595        Option<u64>,
596        // The id of the request.
597        Uuid,
598        // The Storage proxy map.
599        StorageProxyMap,
600    ),
601
602    /// Deletes the database
603    DeleteDatabase(
604        GenericCallback<BackendResult<u64>>,
605        ImmutableOrigin,
606        // Database name.
607        String,
608        // The Storage proxy map.
609        StorageProxyMap,
610        Uuid,
611    ),
612
613    /// Returns the version of the database
614    Version(
615        GenericSender<BackendResult<u64>>,
616        ImmutableOrigin,
617        String, // Database
618    ),
619
620    /// Abort pending database upgrades
621    AbortPendingUpgrades {
622        pending_upgrades: HashMap<String, HashSet<Uuid>>,
623        origin: ImmutableOrigin,
624        proxy_map: StorageProxyMap,
625    },
626
627    NotifyEndOfVersionChange {
628        id: Uuid,
629        name: String,
630        old_version: u64,
631        origin: ImmutableOrigin,
632    },
633
634    /// Send a reply when done cleaning up thread resources and then shut it down
635    Exit(GenericSender<()>),
636}
637
638#[derive(Debug, Deserialize, Serialize)]
639pub enum IndexedDBThreadMsg {
640    Sync(SyncOperation),
641    Async(
642        ImmutableOrigin,
643        String, // Database
644        String, // ObjectStore
645        u64,    // Serial number of the transaction that requests this operation
646        u64,    // Monotonic request id in the transaction
647        IndexedDBTxnMode,
648        AsyncOperation,
649    ),
650    AsyncSchemaOperation {
651        origin: ImmutableOrigin,
652        database_name: String,
653        store_name: String,
654        operation: AsyncSchemaOperation,
655        transaction_serial_number: u64,
656    },
657    EngineTxnBatchComplete {
658        origin: ImmutableOrigin,
659        db_name: String,
660        txn: u64,
661    },
662
663    /// Measure memory used by this thread and send the report over the provided channel.
664    CollectMemoryReport(ReportsChan),
665}
666
667#[cfg(test)]
668mod test {
669    use super::*;
670
671    #[test]
672    fn test_as_singleton() {
673        let key = IndexedDBKeyType::Number(1.0);
674        let key2 = IndexedDBKeyType::Number(2.0);
675        let range = IndexedDBKeyRange::only(key.clone());
676        assert!(range.is_singleton());
677        assert!(range.as_singleton().is_some());
678        let range = IndexedDBKeyRange::new(Some(key), Some(key2.clone()), false, false);
679        assert!(!range.is_singleton());
680        assert!(range.as_singleton().is_none());
681        let full_range = IndexedDBKeyRange::new(None, None, false, false);
682        assert!(!full_range.is_singleton());
683        assert!(full_range.as_singleton().is_none());
684    }
685}