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<i32>,
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<i32>,
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/// Operations that are not executed instantly, but rather added to a
363/// queue that is eventually run.
364#[derive(Debug, Deserialize, MallocSizeOf, Serialize)]
365pub enum AsyncOperation {
366    ReadOnly(AsyncReadOnlyOperation),
367    ReadWrite(AsyncReadWriteOperation),
368}
369
370impl AsyncOperation {
371    pub fn notify_error(&self, error: BackendError) {
372        match self {
373            Self::ReadOnly(operation) => operation.notify_error(error),
374            Self::ReadWrite(operation) => operation.notify_error(error),
375        }
376    }
377}
378
379#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
380pub enum CreateObjectResult {
381    Created,
382    AlreadyExists,
383}
384
385#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
386/// Messaging used in the context of connection lifecycle management.
387pub enum ConnectionMsg {
388    /// Error if a DB is opened for a version lower than the current db version.
389    VersionError { name: String, id: Uuid },
390    /// Opening a connection was aborted.
391    AbortError { name: String, id: Uuid },
392    /// A newly created connection with a version,
393    /// updgraded or not.
394    Connection {
395        name: String,
396        id: Uuid,
397        version: u64,
398        upgraded: bool,
399        // https://w3c.github.io/IndexedDB/#upgrade-transaction-steps
400        // Step 3. Set transaction’s scope to connection’s object store set.
401        object_store_names: Vec<String>,
402    },
403    /// An upgrade transaction for a version started.
404    Upgrade {
405        name: String,
406        id: Uuid,
407        version: u64,
408        old_version: u64,
409        transaction: u64,
410        // https://w3c.github.io/IndexedDB/#upgrade-transaction-steps
411        // Step 3. Set transaction’s scope to connection’s object store set.
412        object_store_names: Vec<String>,
413    },
414    /// A `versionchange` event should be fired for a connection.
415    VersionChange {
416        /// The id of the connection.
417        id: Uuid,
418        /// The name of the connection.
419        name: String,
420        version: u64,
421        old_version: u64,
422    },
423    /// A `blocked` event should be fired for a connection.
424    Blocked {
425        name: String,
426        id: Uuid,
427        version: u64,
428        old_version: u64,
429    },
430    /// A backend error related to the database occurred.
431    DatabaseError {
432        name: String,
433        id: Uuid,
434        error: BackendError,
435    },
436    /// Ask script to recheck whether a transaction can commit now.
437    TxnMaybeCommit { db_name: String, txn: u64 },
438}
439
440#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
441pub struct TxnCompleteMsg {
442    pub origin: ImmutableOrigin,
443    pub db_name: String,
444    pub txn: u64,
445    pub result: BackendResult<()>,
446}
447
448#[derive(Debug, Deserialize, Serialize)]
449pub struct DatabaseInfo {
450    pub name: String,
451    pub version: u64,
452}
453
454#[derive(Debug, Deserialize, Serialize)]
455pub enum SyncOperation {
456    /// Gets existing databases.
457    GetDatabases(
458        GenericCallback<BackendResult<Vec<DatabaseInfo>>>,
459        ImmutableOrigin,
460    ),
461    /// Upgrades the version of the database
462    UpgradeVersion(
463        /// Sender to send new version as the result of the operation
464        GenericSender<BackendResult<u64>>,
465        ImmutableOrigin,
466        String, // Database
467        u64,    // Serial number for the transaction
468        u64,    // Version to upgrade to
469    ),
470    /// Get object store info
471    GetObjectStore(
472        GenericSender<BackendResult<IndexedDBObjectStore>>,
473        ImmutableOrigin,
474        String, // Database
475        String, // Store
476    ),
477    /// Commits changes of a transaction to the database
478    Commit(
479        GenericCallback<TxnCompleteMsg>,
480        ImmutableOrigin,
481        String, // Database
482        u64,    // Transaction serial number
483    ),
484    /// Aborts a transaction in the backend
485    Abort(
486        GenericCallback<TxnCompleteMsg>,
487        ImmutableOrigin,
488        String, // Database
489        u64,    // Transaction serial number
490    ),
491    /// Upgrade transaction finished after its event was fired in script.
492    UpgradeTransactionFinished {
493        origin: ImmutableOrigin,
494        db_name: String,
495        txn: u64,
496        committed: bool,
497    },
498    /// <https://w3c.github.io/IndexedDB/#transaction-lifetime
499    /// Step 3:
500    /// When each request associated with a transaction is processed,
501    /// a success or error event will be fired. While the event is
502    /// being dispatched, the transaction state is set to active, allowing
503    /// additional requests to be made against the transaction. Once the
504    /// event dispatch is complete, the transaction’s state is set to inactive again.
505    RequestHandled {
506        origin: ImmutableOrigin,
507        db_name: String,
508        txn: u64,
509        request_id: u64,
510    },
511    CreateTransaction {
512        sender: GenericSender<BackendResult<u64>>,
513        origin: ImmutableOrigin,
514        db_name: String,
515        mode: IndexedDBTxnMode,
516        scope: Vec<String>,
517    },
518    /// Request script to recheck transaction commit eligibility.
519    TxnMaybeCommit {
520        origin: ImmutableOrigin,
521        db_name: String,
522        txn: u64,
523    },
524    TransactionFinished {
525        origin: ImmutableOrigin,
526        db_name: String,
527        txn: u64,
528    },
529
530    /// Creates a new index for the database
531    CreateIndex(
532        ImmutableOrigin,
533        String,  // Database
534        String,  // Store
535        String,  // Index name
536        KeyPath, // key path
537        bool,    // unique flag
538        bool,    // multientry flag
539    ),
540    /// Delete an index
541    DeleteIndex(
542        ImmutableOrigin,
543        String, // Database
544        String, // Store
545        String, // Index name
546    ),
547
548    /// Creates a new store for the database
549    CreateObjectStore(
550        GenericSender<BackendResult<CreateObjectResult>>,
551        ImmutableOrigin,
552        String,          // Database
553        String,          // Store
554        Option<KeyPath>, // Key Path
555        bool,
556    ),
557
558    DeleteObjectStore(
559        GenericSender<BackendResult<()>>,
560        ImmutableOrigin,
561        String, // Database
562        String, // Store
563    ),
564
565    CloseDatabase(
566        ImmutableOrigin,
567        Uuid,
568        String, // Database
569    ),
570
571    OpenDatabase(
572        // Callback for the result.
573        GenericCallback<ConnectionMsg>,
574        // Origin of the request.
575        ImmutableOrigin,
576        // Name of the database.
577        String,
578        // Requested db version(optional).
579        Option<u64>,
580        // The id of the request.
581        Uuid,
582        // The Storage proxy map.
583        StorageProxyMap,
584    ),
585
586    /// Deletes the database
587    DeleteDatabase(
588        GenericCallback<BackendResult<u64>>,
589        ImmutableOrigin,
590        // Database name.
591        String,
592        // The Storage proxy map.
593        StorageProxyMap,
594        Uuid,
595    ),
596
597    /// Returns the version of the database
598    Version(
599        GenericSender<BackendResult<u64>>,
600        ImmutableOrigin,
601        String, // Database
602    ),
603
604    /// Abort pending database upgrades
605    AbortPendingUpgrades {
606        pending_upgrades: HashMap<String, HashSet<Uuid>>,
607        origin: ImmutableOrigin,
608        proxy_map: StorageProxyMap,
609    },
610
611    NotifyEndOfVersionChange {
612        id: Uuid,
613        name: String,
614        old_version: u64,
615        origin: ImmutableOrigin,
616    },
617
618    /// Send a reply when done cleaning up thread resources and then shut it down
619    Exit(GenericSender<()>),
620}
621
622#[derive(Debug, Deserialize, Serialize)]
623pub enum IndexedDBThreadMsg {
624    Sync(SyncOperation),
625    Async(
626        ImmutableOrigin,
627        String, // Database
628        String, // ObjectStore
629        u64,    // Serial number of the transaction that requests this operation
630        u64,    // Monotonic request id in the transaction
631        IndexedDBTxnMode,
632        AsyncOperation,
633    ),
634    EngineTxnBatchComplete {
635        origin: ImmutableOrigin,
636        db_name: String,
637        txn: u64,
638    },
639
640    /// Measure memory used by this thread and send the report over the provided channel.
641    CollectMemoryReport(ReportsChan),
642}
643
644#[cfg(test)]
645mod test {
646    use super::*;
647
648    #[test]
649    fn test_as_singleton() {
650        let key = IndexedDBKeyType::Number(1.0);
651        let key2 = IndexedDBKeyType::Number(2.0);
652        let range = IndexedDBKeyRange::only(key.clone());
653        assert!(range.is_singleton());
654        assert!(range.as_singleton().is_some());
655        let range = IndexedDBKeyRange::new(Some(key), Some(key2.clone()), false, false);
656        assert!(!range.is_singleton());
657        assert!(range.as_singleton().is_none());
658        let full_range = IndexedDBKeyRange::new(None, None, false, false);
659        assert!(!full_range.is_singleton());
660        assert!(full_range.as_singleton().is_none());
661    }
662}