Skip to main content

script/dom/indexeddb/
idbcursor.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
5#![cfg_attr(crown, allow(crown::jscontext_first_arg))]
6
7use std::cell::Cell;
8
9use dom_struct::dom_struct;
10use js::context::JSContext;
11use js::jsapi::Heap;
12use js::jsval::{JSVal, UndefinedValue};
13use js::rust::MutableHandleValue;
14use script_bindings::cell::DomRefCell;
15use script_bindings::reflector::{Reflector, reflect_dom_object};
16use storage_traits::indexeddb::{IndexedDBKeyRange, IndexedDBKeyType, IndexedDBRecord};
17
18use crate::dom::bindings::codegen::Bindings::IDBCursorBinding::{
19    IDBCursorDirection, IDBCursorMethods,
20};
21use crate::dom::bindings::codegen::UnionTypes::IDBObjectStoreOrIDBIndex;
22use crate::dom::bindings::error::Error;
23use crate::dom::bindings::refcounted::Trusted;
24use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
25use crate::dom::bindings::structuredclone;
26use crate::dom::globalscope::GlobalScope;
27use crate::dom::indexeddb::idbindex::IDBIndex;
28use crate::dom::indexeddb::idbobjectstore::IDBObjectStore;
29use crate::dom::indexeddb::idbrequest::IDBRequest;
30use crate::dom::indexeddb::idbtransaction::IDBTransaction;
31use crate::dom::indexeddb::key::key_type_to_jsval;
32
33#[derive(JSTraceable, MallocSizeOf)]
34#[expect(unused)]
35#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
36pub(crate) enum ObjectStoreOrIndex {
37    ObjectStore(Dom<IDBObjectStore>),
38    Index(Dom<IDBIndex>),
39}
40
41#[dom_struct]
42pub(crate) struct IDBCursor {
43    reflector_: Reflector,
44
45    /// <https://www.w3.org/TR/IndexedDB-3/#cursor-transaction>
46    transaction: Dom<IDBTransaction>,
47    /// <https://www.w3.org/TR/IndexedDB-3/#cursor-range>
48    #[no_trace]
49    range: IndexedDBKeyRange,
50    /// <https://www.w3.org/TR/IndexedDB-3/#cursor-source>
51    source: ObjectStoreOrIndex,
52    /// <https://www.w3.org/TR/IndexedDB-3/#cursor-direction>
53    direction: IDBCursorDirection,
54    /// <https://www.w3.org/TR/IndexedDB-3/#cursor-position>
55    #[no_trace]
56    position: DomRefCell<Option<IndexedDBKeyType>>,
57    /// <https://www.w3.org/TR/IndexedDB-3/#cursor-key>
58    #[no_trace]
59    key: DomRefCell<Option<IndexedDBKeyType>>,
60    #[ignore_malloc_size_of = "mozjs"]
61    cached_key: DomRefCell<Option<Heap<JSVal>>>,
62    #[ignore_malloc_size_of = "mozjs"]
63    cached_primary_key: DomRefCell<Option<Heap<JSVal>>>,
64    /// <https://www.w3.org/TR/IndexedDB-3/#cursor-value>
65    #[ignore_malloc_size_of = "mozjs"]
66    value: Heap<JSVal>,
67    /// <https://www.w3.org/TR/IndexedDB-3/#cursor-got-value-flag>
68    got_value: Cell<bool>,
69    /// <https://www.w3.org/TR/IndexedDB-3/#cursor-object-store-position>
70    #[no_trace]
71    object_store_position: DomRefCell<Option<IndexedDBKeyType>>,
72    /// <https://www.w3.org/TR/IndexedDB-3/#cursor-key-only-flag>
73    key_only: bool,
74
75    /// <https://w3c.github.io/IndexedDB/#cursor-request>
76    request: MutNullableDom<IDBRequest>,
77}
78
79impl IDBCursor {
80    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
81    pub(crate) fn new_inherited(
82        transaction: &IDBTransaction,
83        direction: IDBCursorDirection,
84        got_value: bool,
85        source: ObjectStoreOrIndex,
86        range: IndexedDBKeyRange,
87        key_only: bool,
88    ) -> IDBCursor {
89        IDBCursor {
90            reflector_: Reflector::new(),
91            transaction: Dom::from_ref(transaction),
92            range,
93            source,
94            direction,
95            position: DomRefCell::new(None),
96            key: DomRefCell::new(None),
97            cached_key: DomRefCell::new(None),
98            cached_primary_key: DomRefCell::new(None),
99            value: Heap::default(),
100            got_value: Cell::new(got_value),
101            object_store_position: DomRefCell::new(None),
102            key_only,
103            request: Default::default(),
104        }
105    }
106
107    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
108    #[allow(clippy::too_many_arguments)]
109    pub(crate) fn new(
110        cx: &mut JSContext,
111        global: &GlobalScope,
112        transaction: &IDBTransaction,
113        direction: IDBCursorDirection,
114        got_value: bool,
115        source: ObjectStoreOrIndex,
116        range: IndexedDBKeyRange,
117        key_only: bool,
118    ) -> DomRoot<IDBCursor> {
119        reflect_dom_object(
120            cx,
121            Box::new(IDBCursor::new_inherited(
122                transaction,
123                direction,
124                got_value,
125                source,
126                range,
127                key_only,
128            )),
129            global,
130        )
131    }
132
133    fn set_position(&self, position: Option<IndexedDBKeyType>) {
134        let changed = *self.position.borrow() != position;
135        *self.position.borrow_mut() = position;
136        if changed {
137            *self.cached_primary_key.borrow_mut() = None;
138        }
139    }
140
141    fn set_key(&self, key: Option<IndexedDBKeyType>) {
142        let key_changed = {
143            let current_key = self.key.borrow();
144            current_key.as_ref() != key.as_ref()
145        };
146        *self.key.borrow_mut() = key;
147        if key_changed {
148            *self.cached_key.borrow_mut() = None;
149        }
150    }
151
152    fn set_object_store_position(&self, object_store_position: Option<IndexedDBKeyType>) {
153        let changed = *self.object_store_position.borrow() != object_store_position;
154        *self.object_store_position.borrow_mut() = object_store_position;
155        if changed {
156            *self.cached_primary_key.borrow_mut() = None;
157        }
158    }
159
160    pub(crate) fn set_request(&self, request: &IDBRequest) {
161        self.request.set(Some(request));
162    }
163
164    pub(crate) fn value(&self, mut out: MutableHandleValue) {
165        out.set(self.value.get());
166    }
167
168    /// <https://www.w3.org/TR/IndexedDB-3/#cursor-effective-key>
169    pub(crate) fn effective_key(&self) -> Option<IndexedDBKeyType> {
170        match &self.source {
171            ObjectStoreOrIndex::ObjectStore(_) => self.position.borrow().clone(),
172            ObjectStoreOrIndex::Index(_) => self.object_store_position.borrow().clone(),
173        }
174    }
175}
176
177impl IDBCursorMethods<crate::DomTypeHolder> for IDBCursor {
178    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbcursor-source>
179    fn Source(&self) -> IDBObjectStoreOrIDBIndex {
180        match &self.source {
181            ObjectStoreOrIndex::ObjectStore(source) => {
182                IDBObjectStoreOrIDBIndex::IDBObjectStore(source.as_rooted())
183            },
184            ObjectStoreOrIndex::Index(source) => {
185                IDBObjectStoreOrIDBIndex::IDBIndex(source.as_rooted())
186            },
187        }
188    }
189
190    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbcursor-direction>
191    fn Direction(&self) -> IDBCursorDirection {
192        self.direction
193    }
194
195    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbcursor-key>
196    fn Key(&self, cx: &mut JSContext, mut value: MutableHandleValue) {
197        // The key getter steps are to return the result of converting a key to a value with the cursor’s current key.
198        //
199        // NOTE: If key returns an object (e.g. a Date or Array), it returns the
200        // same object instance every time it is inspected, until the cursor’s key is changed.
201        // This means that if the object is modified, those modifications will be seen by
202        // anyone inspecting the value of the cursor. However modifying such an object does not
203        // modify the contents of the database.
204        if let Some(cached) = &*self.cached_key.borrow() {
205            value.set(cached.get());
206            return;
207        }
208
209        match self.key.borrow().as_ref() {
210            Some(key) => key_type_to_jsval(cx, key, value.reborrow()),
211            None => value.set(UndefinedValue()),
212        }
213
214        *self.cached_key.borrow_mut() = Some(Heap::default());
215        self.cached_key.borrow().as_ref().unwrap().set(value.get());
216    }
217
218    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbcursor-primarykey>
219    fn PrimaryKey(&self, cx: &mut JSContext, mut value: MutableHandleValue) {
220        // NOTE: If primaryKey returns an object (e.g. a Date or Array),
221        // it returns the same object instance every time it is inspected,
222        // until the cursor’s effective key is changed. This means that if the object is modified,
223        // those modifications will be seen by anyone inspecting the value of the cursor.
224        // However modifying such an object does not modify the contents of the database.
225        if let Some(cached) = &*self.cached_primary_key.borrow() {
226            value.set(cached.get());
227            return;
228        }
229
230        match self.effective_key() {
231            Some(effective_key) => key_type_to_jsval(cx, &effective_key, value.reborrow()),
232            None => value.set(UndefinedValue()),
233        }
234
235        *self.cached_primary_key.borrow_mut() = Some(Heap::default());
236        self.cached_primary_key
237            .borrow()
238            .as_ref()
239            .unwrap()
240            .set(value.get());
241    }
242
243    /// <https://w3c.github.io/IndexedDB/#dom-idbcursor-request>
244    fn Request(&self) -> DomRoot<IDBRequest> {
245        self.request
246            .get()
247            .expect("IDBCursor.request should be set when cursor is opened")
248    }
249}
250
251/// A struct containing parameters for
252/// <https://www.w3.org/TR/IndexedDB-3/#iterate-a-cursor>
253#[derive(Clone)]
254pub(crate) struct IterationParam {
255    pub(crate) cursor: Trusted<IDBCursor>,
256    pub(crate) key: Option<IndexedDBKeyType>,
257    pub(crate) primary_key: Option<IndexedDBKeyType>,
258    pub(crate) count: Option<u32>,
259}
260
261/// <https://www.w3.org/TR/IndexedDB-3/#iterate-a-cursor>
262///
263/// NOTE: Be cautious: this part of the specification seems to assume the cursor’s source is an
264/// index. Therefore,
265///   "record’s key" means the key of the record,
266///   "record’s value" means the primary key of the record, and
267///   "record’s referenced value" means the value of the record.
268pub(crate) fn iterate_cursor(
269    global: &GlobalScope,
270    cx: &mut JSContext,
271    param: &IterationParam,
272    records: Vec<IndexedDBRecord>,
273) -> Result<Option<DomRoot<IDBCursor>>, Error> {
274    // Unpack IterationParam
275    let cursor = param.cursor.root();
276    let key = param.key.clone();
277    let primary_key = param.primary_key.clone();
278    let count = param.count;
279
280    // Step 1. Let source be cursor’s source.
281    let source = &cursor.source;
282
283    // Step 2. Let direction be cursor’s direction.
284    let direction = cursor.direction;
285
286    // Step 3. Assert: if primaryKey is given, source is an index and direction is "next" or "prev".
287    if primary_key.is_some() {
288        assert!(matches!(source, ObjectStoreOrIndex::Index(..)));
289        assert!(matches!(
290            direction,
291            IDBCursorDirection::Next | IDBCursorDirection::Prev
292        ));
293    }
294
295    // Step 4. Let records be the list of records in source.
296    // NOTE: It is given as a function parameter.
297
298    // Step 5. Let range be cursor’s range.
299    let range = &cursor.range;
300
301    // Step 6. Let position be cursor’s position.
302    let mut position = cursor.position.borrow().clone();
303
304    // Step 7. Let object store position be cursor’s object store position.
305    let object_store_position = cursor.object_store_position.borrow().clone();
306
307    // Step 8. If count is not given, let count be 1.
308    let mut count = count.unwrap_or(1);
309
310    let mut found_record: Option<&IndexedDBRecord> = None;
311
312    // Step 9. While count is greater than 0:
313    while count > 0 {
314        // Step 9.1. Switch on direction:
315        found_record = match direction {
316            // "next"
317            IDBCursorDirection::Next => records.iter().find(|record| {
318                // Let found record be the first record in records which satisfy all of the
319                // following requirements:
320
321                // If key is defined, the record’s key is greater than or equal to key.
322                let requirement1 = || match &key {
323                    Some(key) => &record.key >= key,
324                    None => true,
325                };
326
327                // If primaryKey is defined, the record’s key is equal to key and the record’s
328                // value is greater than or equal to primaryKey, or the record’s key is greater
329                // than key.
330                let requirement2 = || match &primary_key {
331                    Some(primary_key) => key.as_ref().is_some_and(|key| {
332                        (&record.key == key && &record.primary_key >= primary_key) ||
333                            &record.key > key
334                    }),
335                    _ => true,
336                };
337
338                // If position is defined, and source is an object store, the record’s key is
339                // greater than position.
340                let requirement3 = || match (&position, source) {
341                    (Some(position), ObjectStoreOrIndex::ObjectStore(_)) => &record.key > position,
342                    _ => true,
343                };
344
345                // If position is defined, and source is an index, the record’s key is equal to
346                // position and the record’s value is greater than object store position or the
347                // record’s key is greater than position.
348                let requirement4 = || match (&position, source) {
349                    (Some(position), ObjectStoreOrIndex::Index(_)) => {
350                        (&record.key == position &&
351                            object_store_position.as_ref().is_some_and(
352                                |object_store_position| &record.primary_key > object_store_position,
353                            )) ||
354                            &record.key > position
355                    },
356                    _ => true,
357                };
358
359                // The record’s key is in range.
360                let requirement5 = || range.contains(&record.key);
361
362                // NOTE: Use closures here for lazy computation on requirements.
363                requirement1() &&
364                    requirement2() &&
365                    requirement3() &&
366                    requirement4() &&
367                    requirement5()
368            }),
369            // "nextunique"
370            IDBCursorDirection::Nextunique => records.iter().find(|record| {
371                // Let found record be the first record in records which satisfy all of the
372                // following requirements:
373
374                // If key is defined, the record’s key is greater than or equal to key.
375                let requirement1 = || match &key {
376                    Some(key) => &record.key >= key,
377                    None => true,
378                };
379
380                // If position is defined, the record’s key is greater than position.
381                let requirement2 = || match &position {
382                    Some(position) => &record.key > position,
383                    None => true,
384                };
385
386                // The record’s key is in range.
387                let requirement3 = || range.contains(&record.key);
388
389                // NOTE: Use closures here for lazy computation on requirements.
390                requirement1() && requirement2() && requirement3()
391            }),
392            // "prev"
393            IDBCursorDirection::Prev => {
394                records.iter().rev().find(|&record| {
395                    // Let found record be the last record in records which satisfy all of the
396                    // following requirements:
397
398                    // If key is defined, the record’s key is less than or equal to key.
399                    let requirement1 = || match &key {
400                        Some(key) => &record.key <= key,
401                        None => true,
402                    };
403
404                    // If primaryKey is defined, the record’s key is equal to key and the record’s
405                    // value is less than or equal to primaryKey, or the record’s key is less than
406                    // key.
407                    let requirement2 = || match &primary_key {
408                        Some(primary_key) => key.as_ref().is_some_and(|key| {
409                            (&record.key == key && &record.primary_key <= primary_key) ||
410                                &record.key < key
411                        }),
412                        _ => true,
413                    };
414
415                    // If position is defined, and source is an object store, the record’s key is
416                    // less than position.
417                    let requirement3 = || match (&position, source) {
418                        (Some(position), ObjectStoreOrIndex::ObjectStore(_)) => {
419                            &record.key < position
420                        },
421                        _ => true,
422                    };
423
424                    // If position is defined, and source is an index, the record’s key is equal to
425                    // position and the record’s value is less than object store position or the
426                    // record’s key is less than position.
427                    let requirement4 = || match (&position, source) {
428                        (Some(position), ObjectStoreOrIndex::Index(_)) => {
429                            (&record.key == position &&
430                                object_store_position.as_ref().is_some_and(
431                                    |object_store_position| {
432                                        &record.primary_key < object_store_position
433                                    },
434                                )) ||
435                                &record.key < position
436                        },
437                        _ => true,
438                    };
439
440                    // The record’s key is in range.
441                    let requirement5 = || range.contains(&record.key);
442
443                    // NOTE: Use closures here for lazy computation on requirements.
444                    requirement1() &&
445                        requirement2() &&
446                        requirement3() &&
447                        requirement4() &&
448                        requirement5()
449                })
450            },
451            // "prevunique"
452            IDBCursorDirection::Prevunique => records
453                .iter()
454                .rev()
455                .find(|&record| {
456                    // Let temp record be the last record in records which satisfy all of the
457                    // following requirements:
458
459                    // If key is defined, the record’s key is less than or equal to key.
460                    let requirement1 = || match &key {
461                        Some(key) => &record.key <= key,
462                        None => true,
463                    };
464
465                    // If position is defined, the record’s key is less than position.
466                    let requirement2 = || match &position {
467                        Some(position) => &record.key < position,
468                        None => true,
469                    };
470
471                    // The record’s key is in range.
472                    let requirement3 = || range.contains(&record.key);
473
474                    // NOTE: Use closures here for lazy computation on requirements.
475                    requirement1() && requirement2() && requirement3()
476                })
477                // If temp record is defined, let found record be the first record in records
478                // whose key is equal to temp record’s key.
479                .map(|temp_record| {
480                    records
481                        .iter()
482                        .find(|&record| record.key == temp_record.key)
483                        .expect(
484                            "Record with key equal to temp record's key should exist in records",
485                        )
486                }),
487        };
488
489        match found_record {
490            // Step 9.2. If found record is not defined, then:
491            None => {
492                // Step 9.2.1. Set cursor’s key to undefined.
493                cursor.set_key(None);
494
495                // Step 9.2.2. If source is an index, set cursor’s object store position to undefined.
496                if matches!(source, ObjectStoreOrIndex::Index(_)) {
497                    cursor.set_object_store_position(None);
498                }
499
500                // Step 9.2.3. If cursor’s key only flag is unset, set cursor’s value to undefined.
501                if !cursor.key_only {
502                    cursor.value.set(UndefinedValue());
503                }
504
505                // Step 9.2.4. Return null.
506                return Ok(None);
507            },
508            Some(found_record) => {
509                // Step 9.3. Let position be found record’s key.
510                position = Some(found_record.key.clone());
511
512                // Step 9.4. If source is an index, let object store position be found record’s value.
513                if matches!(source, ObjectStoreOrIndex::Index(_)) {
514                    cursor.set_object_store_position(Some(found_record.primary_key.clone()));
515                }
516
517                // Step 9.5. Decrease count by 1.
518                count -= 1;
519            },
520        }
521    }
522    let found_record =
523        found_record.expect("The while loop above guarantees found_record is defined");
524
525    // Step 10. Set cursor’s position to position.
526    cursor.set_position(position);
527
528    // Step 11. If source is an index, set cursor’s object store position to object store position.
529    if let ObjectStoreOrIndex::Index(_) = source {
530        cursor.set_object_store_position(object_store_position);
531    }
532
533    // Step 12. Set cursor’s key to found record’s key.
534    cursor.set_key(Some(found_record.key.clone()));
535
536    // Step 13. If cursor’s key only flag is unset, then:
537    if !cursor.key_only {
538        // Step 13.1. Let serialized be found record’s referenced value.
539        // Step 13.2. Set cursor’s value to ! StructuredDeserialize(serialized, targetRealm)
540        rooted!(&in(cx) let mut new_cursor_value = UndefinedValue());
541        postcard::from_bytes(&found_record.value)
542            .map_err(|_| Error::Data(None))
543            .and_then(|data| {
544                structuredclone::read(cx, global, data, new_cursor_value.handle_mut())
545            })?;
546        cursor.value.set(new_cursor_value.get());
547    }
548
549    // Step 14. Set cursor’s got value flag.
550    cursor.got_value.set(true);
551
552    // Step 15. Return cursor.
553    Ok(Some(cursor))
554}