sqlite_wasm_vfs/
relaxed_idb.rs

1//! relaxed-idb vfs implementation
2//!
3//! ```rust
4//! use sqlite_wasm_rs as ffi;
5//! use sqlite_wasm_vfs::relaxed_idb::{
6//!     install as install_idb_vfs,
7//!     RelaxedIdbCfg
8//! };
9//!
10//! async fn open_db() {
11//!     // install relaxed-idb persistent vfs and set as default vfs
12//!     install_idb_vfs::<ffi::WasmOsCallback>(&RelaxedIdbCfg::default(), true)
13//!         .await
14//!         .unwrap();
15//!
16//!     // open with relaxed-idb vfs
17//!     let mut db = std::ptr::null_mut();
18//!     let ret = unsafe {
19//!         ffi::sqlite3_open_v2(
20//!             c"relaxed-idb.db".as_ptr().cast(),
21//!             &mut db as *mut _,
22//!             ffi::SQLITE_OPEN_READWRITE | ffi::SQLITE_OPEN_CREATE,
23//!             std::ptr::null()
24//!         )
25//!     };
26//!     assert_eq!(ffi::SQLITE_OK, ret);
27//! }
28//! ```
29//!
30//! Inspired by wa-sqlite's [`IDBMirrorVFS`](https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/IDBMirrorVFS.js),
31//! this is an VFS used in a synchronization context.
32//!
33//! The principle is to preload the db into memory before xOpen, and then all operations are synchronous.
34//! When sqlite calls sync, it asynchronously writes the changed blocks to the indexed db through the indexed transaction.
35//! The difference from IDBMirrorVFS is that `RelaxedIdbVFS` does only support pragma `synchronous=off`.
36//!
37//! As for performance, since both reading and writing are done in memory, the performance is very good.
38//! However, we need to pay attention to the performance of preload the database, because the database is divided
39//! into multiple blocks and stored in the indexed db, and it takes some time to read all of them into memory.
40//! After my test, when page_size is 64k, the loading speed is the fastest.
41//!
42//! As with MemoryVFS, you also need to pay attention to the memory size limit of the browser page.
43//!
44//! It is particularly important to note that using it on multiple pages may cause DB corruption.
45//! It is recommended to use it in SharedWorker.
46
47use rsqlite_vfs::{
48    bail, check_db_and_page_size, check_import_db, check_option, check_result,
49    ffi::{
50        sqlite3_file, sqlite3_vfs, SQLITE_ERROR, SQLITE_FCNTL_COMMIT_PHASETWO, SQLITE_FCNTL_PRAGMA,
51        SQLITE_FCNTL_SYNC, SQLITE_IOERR, SQLITE_IOERR_DELETE, SQLITE_NOTFOUND, SQLITE_OK,
52        SQLITE_OPEN_MAIN_DB,
53    },
54    register_vfs, registered_vfs, ImportDbError, MemChunksFile, OsCallback, RegisterVfsError,
55    SQLiteIoMethods, SQLiteVfs, SQLiteVfsFile, VfsAppData, VfsError, VfsFile, VfsResult, VfsStore,
56};
57use std::time::Duration;
58use std::{cell::RefCell, marker::PhantomData};
59
60use indexed_db_futures::database::Database;
61use indexed_db_futures::prelude::*;
62use indexed_db_futures::transaction::TransactionMode;
63use js_sys::{Number, Object, Reflect, Uint8Array};
64use std::collections::{hash_map, HashSet};
65use std::future::Future;
66use std::pin::Pin;
67use std::task::{Context, Poll};
68use std::{
69    collections::HashMap,
70    ffi::{c_char, CStr},
71};
72use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
73use wasm_bindgen::JsValue;
74
75type Result<T> = std::result::Result<T, RelaxedIdbError>;
76
77fn page_read<T, G: Fn(usize) -> Option<T>, R: Fn(T, &mut [u8], (usize, usize))>(
78    buf: &mut [u8],
79    page_size: usize,
80    file_size: usize,
81    offset: usize,
82    get_page: G,
83    read_fn: R,
84) -> bool {
85    if page_size == 0 || file_size == 0 {
86        buf.fill(0);
87        return false;
88    }
89
90    let mut bytes_read = 0;
91    let mut p_data_offset = 0;
92    let p_data_length = buf.len();
93    let i_offset = offset;
94
95    while p_data_offset < p_data_length {
96        let file_offset = i_offset + p_data_offset;
97        let page_idx = file_offset / page_size;
98        let page_offset = file_offset % page_size;
99        let page_addr = page_idx * page_size;
100
101        let Some(page) = get_page(page_addr) else {
102            break;
103        };
104
105        let page_length = (page_size - page_offset).min(p_data_length - p_data_offset);
106        read_fn(
107            page,
108            &mut buf[p_data_offset..p_data_offset + page_length],
109            (page_offset, page_offset + page_length),
110        );
111
112        p_data_offset += page_length;
113        bytes_read += page_length;
114    }
115
116    if bytes_read < p_data_length {
117        buf[bytes_read..].fill(0);
118        return false;
119    }
120
121    true
122}
123
124struct IdbCommit {
125    op: IdbCommitOp,
126    notify: Option<tokio::sync::oneshot::Sender<Result<()>>>,
127}
128
129enum IdbCommitOp {
130    Sync(String),
131    Delete(String),
132    Clear,
133}
134
135enum IdbFile {
136    Main(IdbPageFile),
137    Temp(MemChunksFile),
138}
139
140impl IdbFile {
141    fn new(flags: i32) -> Self {
142        if flags & SQLITE_OPEN_MAIN_DB == 0 {
143            Self::Temp(MemChunksFile::default())
144        } else {
145            Self::Main(IdbPageFile::default())
146        }
147    }
148}
149
150#[derive(Default)]
151struct IdbPageFile {
152    file_size: usize,
153    block_size: usize,
154    blocks: HashMap<usize, Uint8Array>,
155    tx_blocks: HashSet<usize>,
156    sync_notified: bool,
157}
158
159impl VfsFile for IdbPageFile {
160    fn read(&self, buf: &mut [u8], offset: usize) -> VfsResult<bool> {
161        Ok(page_read(
162            buf,
163            self.block_size,
164            self.file_size,
165            offset,
166            |addr| self.blocks.get(&addr),
167            |page, buf, (start, end)| {
168                page.subarray(start as u32, end as u32).copy_to(buf);
169            },
170        ))
171    }
172
173    fn write(&mut self, buf: &[u8], offset: usize) -> VfsResult<()> {
174        let page_size = buf.len();
175
176        for fill in (self.file_size..offset).step_by(page_size) {
177            self.blocks
178                .insert(fill, Uint8Array::new_with_length(page_size as u32));
179            self.tx_blocks.insert(fill);
180        }
181
182        if let Some(buffer) = self.blocks.get_mut(&offset) {
183            buffer.copy_from(buf);
184        } else {
185            self.blocks.insert(offset, Uint8Array::new_from_slice(buf));
186        }
187
188        self.tx_blocks.insert(offset);
189        self.block_size = page_size;
190        self.file_size = self.file_size.max(offset + page_size);
191        Ok(())
192    }
193
194    fn truncate(&mut self, size: usize) -> VfsResult<()> {
195        self.file_size = size;
196        Ok(())
197    }
198
199    fn flush(&mut self) -> VfsResult<()> {
200        Ok(())
201    }
202
203    fn size(&self) -> VfsResult<usize> {
204        Ok(self.file_size)
205    }
206}
207
208impl VfsFile for IdbFile {
209    fn read(&self, buf: &mut [u8], offset: usize) -> VfsResult<bool> {
210        match self {
211            IdbFile::Main(idb_page_file) => idb_page_file.read(buf, offset),
212            IdbFile::Temp(mem_chunks_file) => mem_chunks_file.read(buf, offset),
213        }
214    }
215
216    fn write(&mut self, buf: &[u8], offset: usize) -> VfsResult<()> {
217        match self {
218            IdbFile::Main(idb_page_file) => idb_page_file.write(buf, offset),
219            IdbFile::Temp(mem_chunks_file) => mem_chunks_file.write(buf, offset),
220        }
221    }
222
223    fn truncate(&mut self, size: usize) -> VfsResult<()> {
224        match self {
225            IdbFile::Main(idb_page_file) => idb_page_file.truncate(size),
226            IdbFile::Temp(mem_chunks_file) => mem_chunks_file.truncate(size),
227        }
228    }
229
230    fn flush(&mut self) -> VfsResult<()> {
231        match self {
232            IdbFile::Main(idb_page_file) => idb_page_file.flush(),
233            IdbFile::Temp(mem_chunks_file) => mem_chunks_file.flush(),
234        }
235    }
236
237    fn size(&self) -> VfsResult<usize> {
238        match self {
239            IdbFile::Main(idb_page_file) => idb_page_file.size(),
240            IdbFile::Temp(mem_chunks_file) => mem_chunks_file.size(),
241        }
242    }
243}
244
245fn key_range(file: &str, start: usize) -> std::ops::RangeInclusive<[JsValue; 2]> {
246    [JsValue::from(file), JsValue::from(start)]
247        ..=[
248            JsValue::from(file),
249            JsValue::from(Number::POSITIVE_INFINITY),
250        ]
251}
252
253async fn clear_impl(indexed_db: &Database) -> Result<()> {
254    let transaction = indexed_db
255        .transaction("blocks")
256        .with_mode(TransactionMode::Readwrite)
257        .build()?;
258    let blocks = transaction.object_store("blocks")?;
259    blocks.clear()?;
260    transaction.commit().await?;
261    Ok(())
262}
263
264async fn preload_db_impl(
265    indexed_db: &Database,
266    preload: &Preload,
267) -> Result<HashMap<String, IdbFile>> {
268    if matches!(preload, &Preload::None) {
269        return Ok(HashMap::new());
270    }
271
272    let transaction = indexed_db
273        .transaction("blocks")
274        .with_mode(TransactionMode::Readonly)
275        .build()?;
276    let blocks = transaction.object_store("blocks")?;
277
278    let mut name2file = HashMap::new();
279    let mut insert_fn = |block: JsValue| {
280        let (path, offset, data) = get_block(block);
281        match name2file.entry(path) {
282            hash_map::Entry::Occupied(mut occupied_entry) => {
283                let IdbFile::Main(db) = occupied_entry.get_mut() else {
284                    unreachable!();
285                };
286                db.file_size += db.block_size;
287                db.blocks.insert(offset, data);
288            }
289            hash_map::Entry::Vacant(vacant_entry) => {
290                vacant_entry.insert(IdbFile::Main(IdbPageFile {
291                    file_size: data.length() as _,
292                    block_size: data.length() as _,
293                    blocks: HashMap::from([(offset, data)]),
294                    tx_blocks: HashSet::new(),
295                    sync_notified: false,
296                }));
297            }
298        }
299    };
300
301    match preload {
302        Preload::All => {
303            for block in blocks.get_all::<JsValue>().await? {
304                insert_fn(block?);
305            }
306        }
307        Preload::Paths(items) => {
308            for file in items {
309                for block in blocks
310                    .get_all::<JsValue>()
311                    .with_query(key_range(file, 0))
312                    .await?
313                {
314                    insert_fn(block?);
315                }
316            }
317        }
318        Preload::None => unreachable!(),
319    }
320
321    Ok(name2file)
322}
323
324struct RelaxedIdb {
325    idb: Database,
326    name2file: RefCell<HashMap<String, IdbFile>>,
327    tx: UnboundedSender<IdbCommit>,
328}
329
330impl RelaxedIdb {
331    async fn new(options: &RelaxedIdbCfg, tx: UnboundedSender<IdbCommit>) -> Result<Self> {
332        let indexed_db = Database::open(&options.vfs_name)
333            .with_version(1u8)
334            .with_on_upgrade_needed(|_, db| {
335                db.create_object_store("blocks")
336                    .with_key_path(["path", "offset"].into())
337                    .build()?;
338                Ok(())
339            })
340            .await?;
341
342        if options.clear_on_init {
343            clear_impl(&indexed_db).await?;
344        }
345
346        let name2file = preload_db_impl(&indexed_db, &options.preload).await?;
347        Ok(RelaxedIdb {
348            idb: indexed_db,
349            name2file: RefCell::new(name2file),
350            tx,
351        })
352    }
353
354    fn send_task(&self, op: IdbCommitOp) -> Result<()> {
355        if self.tx.send(IdbCommit { op, notify: None }).is_err() {
356            return Err(RelaxedIdbError::Generic(
357                "failed to send commit task".into(),
358            ));
359        }
360        Ok(())
361    }
362
363    fn send_task_with_notify(&self, op: IdbCommitOp) -> Result<WaitCommit> {
364        let (tx, rx) = tokio::sync::oneshot::channel();
365        let commit = IdbCommit {
366            op,
367            notify: Some(tx),
368        };
369        if self.tx.send(commit).is_err() {
370            return Err(RelaxedIdbError::Generic(
371                "failed to send commit task".into(),
372            ));
373        }
374        Ok(WaitCommit(rx))
375    }
376
377    async fn preload_db(&self, files: Vec<String>) -> Result<()> {
378        let preload = {
379            let name2file = self.name2file.borrow();
380            files
381                .into_iter()
382                .filter(|x| !name2file.contains_key(x))
383                .collect::<Vec<_>>()
384        };
385        let preload = preload_db_impl(&self.idb, &Preload::Paths(preload)).await?;
386        self.name2file.borrow_mut().extend(preload);
387        Ok(())
388    }
389
390    fn import_db(&self, filename: &str, bytes: &[u8]) -> Result<WaitCommit> {
391        let page_size = check_import_db(bytes)?;
392        self.import_db_unchecked(filename, bytes, page_size, true)
393    }
394
395    fn import_db_unchecked(
396        &self,
397        filename: &str,
398        bytes: &[u8],
399        page_size: usize,
400        clear_wal: bool,
401    ) -> Result<WaitCommit> {
402        check_db_and_page_size(bytes.len(), page_size)?;
403
404        if self.name2file.borrow().contains_key(filename) {
405            return Err(RelaxedIdbError::Generic(format!(
406                "{filename} file already exists"
407            )));
408        }
409
410        let mut blocks: HashMap<usize, Uint8Array> = bytes
411            .chunks(page_size)
412            .enumerate()
413            .map(|(idx, buffer)| (idx * page_size, Uint8Array::new_from_slice(buffer)))
414            .collect();
415
416        // forced to write back to legacy mode
417        if clear_wal {
418            let header = blocks.get_mut(&0).unwrap();
419            header.subarray(18, 20).copy_from(&[1, 1]);
420        }
421
422        let tx_blocks = blocks.keys().copied().collect();
423
424        self.name2file.borrow_mut().insert(
425            filename.into(),
426            IdbFile::Main(IdbPageFile {
427                file_size: blocks.len() * page_size,
428                block_size: page_size,
429                blocks,
430                tx_blocks,
431                sync_notified: false,
432            }),
433        );
434
435        self.send_task_with_notify(IdbCommitOp::Sync(filename.into()))
436    }
437
438    fn export_db(&self, name: &str) -> Result<Vec<u8>> {
439        let name2file = self.name2file.borrow();
440
441        match name2file.get(name) {
442            Some(IdbFile::Main(file)) => {
443                let file_size = file.file_size;
444                let mut ret = vec![0; file_size];
445                for (&offset, buffer) in &file.blocks {
446                    if offset >= file_size {
447                        continue;
448                    }
449                    buffer.copy_to(&mut ret[offset..offset + file.block_size]);
450                }
451                Ok(ret)
452            }
453            Some(IdbFile::Temp(_)) => Err(RelaxedIdbError::Generic(
454                "Does not support dumping temporary files".into(),
455            )),
456            None => Err(RelaxedIdbError::Generic(
457                "The file to be exported does not exist".into(),
458            )),
459        }
460    }
461
462    fn delete_db(&self, name: &str) -> Result<WaitCommit> {
463        self.name2file.borrow_mut().remove(name);
464        self.send_task_with_notify(IdbCommitOp::Delete(name.into()))
465    }
466
467    fn clear_all(&self) -> Result<WaitCommit> {
468        std::mem::take(&mut *self.name2file.borrow_mut());
469        self.send_task_with_notify(IdbCommitOp::Clear)
470    }
471
472    fn exists(&self, file: &str) -> bool {
473        self.name2file.borrow().contains_key(file)
474    }
475
476    async fn delete_db_impl(&self, file: &str) -> Result<()> {
477        let transaction = self
478            .idb
479            .transaction("blocks")
480            .with_mode(TransactionMode::Readwrite)
481            .build()?;
482
483        let store = transaction.object_store("blocks")?;
484
485        store.delete(key_range(file, 0)).build()?;
486        transaction.commit().await?;
487
488        Ok(())
489    }
490
491    // already drop
492    #[allow(clippy::await_holding_refcell_ref)]
493    async fn sync_db_impl(&self, file: &str) -> Result<()> {
494        let mut name2file = self.name2file.borrow_mut();
495        let Some(idb_file) = name2file.get_mut(file) else {
496            return Ok(());
497        };
498
499        let IdbFile::Main(idb_blocks) = idb_file else {
500            return Ok(());
501        };
502
503        idb_blocks.sync_notified = false;
504
505        let file_size = idb_blocks.file_size;
506        let mut truncated_offset = idb_blocks.file_size;
507        while idb_blocks.blocks.remove(&truncated_offset).is_some() {
508            truncated_offset += idb_blocks.block_size;
509        }
510
511        let tx_blocks = std::mem::take(&mut idb_blocks.tx_blocks);
512        if tx_blocks.is_empty() && file_size == truncated_offset {
513            // no need to put or delete
514            return Ok(());
515        }
516
517        let path = JsValue::from(file);
518
519        let transaction = self
520            .idb
521            .transaction("blocks")
522            .with_mode(TransactionMode::Readwrite)
523            .build()?;
524
525        let store = transaction.object_store("blocks")?;
526
527        for offset in tx_blocks {
528            if let Some(buffer) = idb_blocks.blocks.get(&offset) {
529                store.put(&set_block(&path, offset, buffer)).build()?;
530            }
531        }
532        store.delete(key_range(file, file_size)).build()?;
533
534        // The `RefMut` from `name2file` is explicitly dropped here to avoid holding the borrow across an `.await` point.
535        drop(name2file);
536
537        transaction.commit().await?;
538
539        Ok(())
540    }
541
542    async fn commit_loop(&self, mut rx: UnboundedReceiver<IdbCommit>) {
543        while let Some(commit) = rx.recv().await {
544            let IdbCommit { op, notify } = commit;
545            let ret = match op {
546                IdbCommitOp::Sync(file) => self.sync_db_impl(&file).await,
547                IdbCommitOp::Delete(file) => self.delete_db_impl(&file).await,
548                IdbCommitOp::Clear => clear_impl(&self.idb).await,
549            };
550            if let Some(notify) = notify {
551                // An unsuccessful send would be one where the corresponding receiver
552                // has already been deallocated.
553                let _ = notify.send(ret);
554            }
555        }
556    }
557}
558
559fn get_block(value: JsValue) -> (String, usize, Uint8Array) {
560    let path = Reflect::get(&value, &JsValue::from("path"))
561        .unwrap()
562        .as_string()
563        .unwrap();
564    let offset = Reflect::get(&value, &JsValue::from("offset"))
565        .unwrap()
566        .as_f64()
567        .unwrap() as usize;
568    let data = Reflect::get(&value, &JsValue::from("data")).unwrap();
569
570    (path, offset, Uint8Array::from(data))
571}
572
573fn set_block(path: &JsValue, offset: usize, data: &Uint8Array) -> JsValue {
574    let block = Object::new();
575    Reflect::set(&block, &JsValue::from("path"), path).unwrap();
576    Reflect::set(&block, &JsValue::from("offset"), &JsValue::from(offset)).unwrap();
577    Reflect::set(&block, &JsValue::from("data"), &JsValue::from(data)).unwrap();
578    block.into()
579}
580
581struct RelaxedIdbStore;
582
583impl VfsStore<IdbFile, RelaxedIdb> for RelaxedIdbStore {
584    fn add_file(vfs: *mut sqlite3_vfs, file: &str, flags: i32) -> VfsResult<()> {
585        let pool = unsafe { Self::app_data(vfs) };
586        pool.name2file
587            .borrow_mut()
588            .insert(file.into(), IdbFile::new(flags));
589        Ok(())
590    }
591
592    fn contains_file(vfs: *mut sqlite3_vfs, file: &str) -> VfsResult<bool> {
593        let pool = unsafe { Self::app_data(vfs) };
594        Ok(pool.name2file.borrow().contains_key(file))
595    }
596
597    fn delete_file(vfs: *mut sqlite3_vfs, file: &str) -> VfsResult<()> {
598        let pool = unsafe { Self::app_data(vfs) };
599        let idb_file = match pool.name2file.borrow_mut().remove(file) {
600            Some(file) => file,
601            None => {
602                return Err(VfsError::new(
603                    SQLITE_IOERR_DELETE,
604                    format!("{file} not found"),
605                ))
606            }
607        };
608        // temp db never put into indexed db, no need to delete
609        if let IdbFile::Main(_) = &idb_file {
610            if pool.send_task(IdbCommitOp::Delete(file.into())).is_err() {
611                return Err(VfsError::new(
612                    SQLITE_IOERR_DELETE,
613                    format!("failed to send delete task, file: {file}"),
614                ));
615            }
616        }
617        Ok(())
618    }
619
620    fn with_file<F: Fn(&IdbFile) -> VfsResult<i32>>(
621        vfs_file: &SQLiteVfsFile,
622        f: F,
623    ) -> VfsResult<i32> {
624        let name = unsafe { vfs_file.name() };
625        let pool = unsafe { Self::app_data(vfs_file.vfs) };
626        match pool.name2file.borrow().get(name) {
627            Some(file) => f(file),
628            None => Err(VfsError::new(SQLITE_IOERR, format!("{name} not found"))),
629        }
630    }
631
632    fn with_file_mut<F: Fn(&mut IdbFile) -> VfsResult<i32>>(
633        vfs_file: &SQLiteVfsFile,
634        f: F,
635    ) -> VfsResult<i32> {
636        let name = unsafe { vfs_file.name() };
637        let pool = unsafe { Self::app_data(vfs_file.vfs) };
638        match pool.name2file.borrow_mut().get_mut(name) {
639            Some(file) => f(file),
640            None => Err(VfsError::new(SQLITE_IOERR, format!("{name} not found"))),
641        }
642    }
643}
644
645struct RelaxedIdbIoMethods;
646
647impl SQLiteIoMethods for RelaxedIdbIoMethods {
648    type File = IdbFile;
649    type AppData = RelaxedIdb;
650    type Store = RelaxedIdbStore;
651
652    const VERSION: ::std::os::raw::c_int = 1;
653
654    unsafe extern "C" fn xFileControl(
655        pFile: *mut sqlite3_file,
656        op: ::std::os::raw::c_int,
657        pArg: *mut ::std::os::raw::c_void,
658    ) -> ::std::os::raw::c_int {
659        let vfs_file = SQLiteVfsFile::from_file(pFile);
660        let pool = Self::Store::app_data(vfs_file.vfs);
661        let name = vfs_file.name();
662
663        let mut name2file = pool.name2file.borrow_mut();
664        let file = check_option!(name2file.get_mut(name));
665
666        let IdbFile::Main(file) = file else {
667            return SQLITE_NOTFOUND;
668        };
669
670        match op {
671            SQLITE_FCNTL_PRAGMA => {
672                let pArg = pArg as *mut *mut c_char;
673                let name = *pArg.add(1);
674                let value = *pArg.add(2);
675
676                bail!(name.is_null());
677                bail!(value.is_null(), SQLITE_NOTFOUND);
678
679                let key = check_result!(CStr::from_ptr(name).to_str());
680                let value = check_result!(CStr::from_ptr(value).to_str());
681
682                if key.eq_ignore_ascii_case("page_size") {
683                    let page_size = check_result!(value.parse::<usize>());
684                    if page_size == file.block_size {
685                        return SQLITE_OK;
686                    } else if file.block_size == 0 {
687                        file.block_size = page_size;
688                    } else {
689                        return pool.store_err(VfsError::new(
690                            SQLITE_ERROR,
691                            "page_size cannot be changed".into(),
692                        ));
693                    }
694                } else if key.eq_ignore_ascii_case("synchronous")
695                    && !value.eq_ignore_ascii_case("off")
696                {
697                    return pool.store_err(VfsError::new(
698                        SQLITE_ERROR,
699                        "relaxed-idb vfs only supports synchronous=off".into(),
700                    ));
701                };
702            }
703            SQLITE_FCNTL_SYNC | SQLITE_FCNTL_COMMIT_PHASETWO => {
704                if !file.sync_notified {
705                    if pool.send_task(IdbCommitOp::Sync(name.into())).is_err() {
706                        return pool.store_err(VfsError::new(
707                            SQLITE_ERROR,
708                            format!("failed to send sync task, file: {name}"),
709                        ));
710                    }
711                    file.sync_notified = true;
712                }
713            }
714            _ => (),
715        }
716
717        SQLITE_NOTFOUND
718    }
719}
720
721struct RelaxedIdbVfs<C>(PhantomData<C>);
722
723impl<C> SQLiteVfs<RelaxedIdbIoMethods> for RelaxedIdbVfs<C>
724where
725    C: OsCallback,
726{
727    const VERSION: ::std::os::raw::c_int = 1;
728
729    fn sleep(dur: Duration) {
730        C::sleep(dur);
731    }
732
733    fn random(buf: &mut [u8]) {
734        C::random(buf);
735    }
736
737    fn epoch_timestamp_in_ms() -> i64 {
738        C::epoch_timestamp_in_ms()
739    }
740}
741
742/// A future that resolves when a pending IndexedDB commit operation is complete.
743pub struct WaitCommit(tokio::sync::oneshot::Receiver<Result<()>>);
744
745impl Future for WaitCommit {
746    type Output = Result<()>;
747
748    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
749        match Pin::new(&mut self.0).poll(cx) {
750            Poll::Ready(ret) => Poll::Ready(ret.unwrap_or_else(|_| {
751                Err(RelaxedIdbError::Generic(
752                    "Waiting for notify failure".into(),
753                ))
754            })),
755            Poll::Pending => Poll::Pending,
756        }
757    }
758}
759
760#[derive(thiserror::Error, Debug)]
761pub enum RelaxedIdbError {
762    #[error(transparent)]
763    Vfs(#[from] RegisterVfsError),
764    #[error(transparent)]
765    ImportDb(#[from] ImportDbError),
766    #[error(transparent)]
767    OpenDb(#[from] indexed_db_futures::error::OpenDbError),
768    #[error(transparent)]
769    IndexedDb(#[from] indexed_db_futures::error::Error),
770    #[error("Generic error: {0}")]
771    Generic(String),
772}
773
774/// Select which dbs to preload into memory.
775pub enum Preload {
776    /// Preload all databases
777    All,
778    /// Specify the path to load the database
779    Paths(Vec<String>),
780    /// Not preloaded, can be manually loaded later via `RelaxedIdbUtil`
781    None,
782}
783
784/// Build `RelaxedIdbCfg`
785pub struct RelaxedIdbCfgBuilder(RelaxedIdbCfg);
786
787impl RelaxedIdbCfgBuilder {
788    pub fn new() -> Self {
789        Self(RelaxedIdbCfg::default())
790    }
791
792    /// The SQLite VFS name under which this pool's VFS is registered.
793    pub fn vfs_name(mut self, name: &str) -> Self {
794        self.0.vfs_name = name.into();
795        self
796    }
797
798    /// Delete all files on initialization.
799    pub fn clear_on_init(mut self, set: bool) -> Self {
800        self.0.clear_on_init = set;
801        self
802    }
803
804    /// Select which dbs to preload into memory.
805    pub fn preload(mut self, preload: Preload) -> Self {
806        self.0.preload = preload;
807        self
808    }
809
810    /// Build `RelaxedIdbCfg`.
811    pub fn build(self) -> RelaxedIdbCfg {
812        self.0
813    }
814}
815
816impl Default for RelaxedIdbCfgBuilder {
817    fn default() -> Self {
818        Self::new()
819    }
820}
821
822/// `RelaxedIdb` options
823pub struct RelaxedIdbCfg {
824    /// The SQLite VFS name under which this pool's VFS is registered.
825    pub vfs_name: String,
826    /// Delete all files on initialization.
827    pub clear_on_init: bool,
828    /// Select which dbs to preload into memory.
829    pub preload: Preload,
830}
831
832impl Default for RelaxedIdbCfg {
833    fn default() -> Self {
834        Self {
835            vfs_name: "relaxed-idb".into(),
836            clear_on_init: false,
837            preload: Preload::All,
838        }
839    }
840}
841
842/// RelaxedIdbVfs management tool.
843pub struct RelaxedIdbUtil {
844    pool: &'static VfsAppData<RelaxedIdb>,
845}
846
847impl RelaxedIdbUtil {
848    /// Preload the db.
849    ///
850    /// Because indexed db reading data is an asynchronous operation,
851    /// the db must be preloaded into memory before opening the sqlite db.
852    pub async fn preload_db(&self, preload: Vec<String>) -> Result<()> {
853        self.pool.preload_db(preload).await
854    }
855
856    /// Import the database.
857    ///
858    /// If the database is imported with WAL mode enabled,
859    /// it will be forced to write back to legacy mode, see
860    /// <https://sqlite.org/forum/forumpost/67882c5b04>
861    ///
862    /// If the imported database is encrypted, use `import_db_unchecked` instead.
863    pub fn import_db(&self, filename: &str, bytes: &[u8]) -> Result<WaitCommit> {
864        self.pool.import_db(filename, bytes)
865    }
866
867    /// `import_db` without checking, can be used to import encrypted database.
868    pub fn import_db_unchecked(
869        &self,
870        filename: &str,
871        bytes: &[u8],
872        page_size: usize,
873    ) -> Result<WaitCommit> {
874        self.pool
875            .import_db_unchecked(filename, bytes, page_size, false)
876    }
877
878    /// Export the database.
879    pub fn export_db(&self, filename: &str) -> Result<Vec<u8>> {
880        self.pool.export_db(filename)
881    }
882
883    /// Delete the specified database, make sure that the database is closed.
884    pub fn delete_db(&self, filename: &str) -> Result<WaitCommit> {
885        self.pool.delete_db(filename)
886    }
887
888    /// Delete all database, make sure that all database is closed.
889    pub fn clear_all(&self) -> Result<WaitCommit> {
890        self.pool.clear_all()
891    }
892
893    /// Does the database exists.
894    pub fn exists(&self, filename: &str) -> bool {
895        self.pool.exists(filename)
896    }
897
898    /// List all files.
899    pub fn list(&self) -> Vec<String> {
900        self.pool.name2file.borrow().keys().cloned().collect()
901    }
902
903    /// Number of files.
904    pub fn count(&self) -> usize {
905        self.pool.name2file.borrow().len()
906    }
907}
908
909/// Register `relaxed-idb` vfs and return a management tool which can be used
910/// to perform basic administration of the file pool.
911///
912/// If the vfs corresponding to `options.vfs_name` has been registered,
913/// only return a management tool without register.
914pub async fn install<C: OsCallback>(
915    options: &RelaxedIdbCfg,
916    default_vfs: bool,
917) -> Result<RelaxedIdbUtil> {
918    static REGISTER_GUARD: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
919    let _guard = REGISTER_GUARD.lock().await;
920
921    let pool = if let Some(vfs) = registered_vfs(&options.vfs_name)? {
922        unsafe { RelaxedIdbStore::app_data(vfs) }
923    } else {
924        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
925        let pool = RelaxedIdb::new(options, tx).await?;
926        let vfs = register_vfs::<RelaxedIdbIoMethods, RelaxedIdbVfs<C>>(
927            &options.vfs_name,
928            pool,
929            default_vfs,
930        )?;
931
932        let app_data = unsafe { RelaxedIdbStore::app_data(vfs) };
933        wasm_bindgen_futures::spawn_local(app_data.commit_loop(rx));
934        app_data
935    };
936
937    Ok(RelaxedIdbUtil { pool })
938}
939
940#[cfg(test)]
941mod tests {
942    use super::{IdbFile, RelaxedIdb, RelaxedIdbCfgBuilder, RelaxedIdbStore};
943    use rsqlite_vfs::{test_suite::test_vfs_store, VfsAppData};
944    use wasm_bindgen_test::wasm_bindgen_test;
945
946    #[wasm_bindgen_test]
947    async fn test_relaxed_idb_vfs_store() {
948        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
949        test_vfs_store::<RelaxedIdb, IdbFile, RelaxedIdbStore>(VfsAppData::new(
950            RelaxedIdb::new(
951                &RelaxedIdbCfgBuilder::new()
952                    .vfs_name("test_relaxed_idb_suite")
953                    .build(),
954                tx,
955            )
956            .await
957            .unwrap(),
958        ))
959        .unwrap();
960
961        wasm_bindgen_futures::spawn_local(async move { while let Some(_) = rx.recv().await {} });
962    }
963}