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