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