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