1use 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 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 #[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 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 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 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 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
742pub 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
774pub enum Preload {
776 All,
778 Paths(Vec<String>),
780 None,
782}
783
784pub struct RelaxedIdbCfgBuilder(RelaxedIdbCfg);
786
787impl RelaxedIdbCfgBuilder {
788 pub fn new() -> Self {
789 Self(RelaxedIdbCfg::default())
790 }
791
792 pub fn vfs_name(mut self, name: &str) -> Self {
794 self.0.vfs_name = name.into();
795 self
796 }
797
798 pub fn clear_on_init(mut self, set: bool) -> Self {
800 self.0.clear_on_init = set;
801 self
802 }
803
804 pub fn preload(mut self, preload: Preload) -> Self {
806 self.0.preload = preload;
807 self
808 }
809
810 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
822pub struct RelaxedIdbCfg {
824 pub vfs_name: String,
826 pub clear_on_init: bool,
828 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
842pub struct RelaxedIdbUtil {
844 pool: &'static VfsAppData<RelaxedIdb>,
845}
846
847impl RelaxedIdbUtil {
848 pub async fn preload_db(&self, preload: Vec<String>) -> Result<()> {
853 self.pool.preload_db(preload).await
854 }
855
856 pub fn import_db(&self, filename: &str, bytes: &[u8]) -> Result<WaitCommit> {
864 self.pool.import_db(filename, bytes)
865 }
866
867 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 pub fn export_db(&self, filename: &str) -> Result<Vec<u8>> {
880 self.pool.export_db(filename)
881 }
882
883 pub fn delete_db(&self, filename: &str) -> Result<WaitCommit> {
885 self.pool.delete_db(filename)
886 }
887
888 pub fn clear_all(&self) -> Result<WaitCommit> {
890 self.pool.clear_all()
891 }
892
893 pub fn exists(&self, filename: &str) -> bool {
895 self.pool.exists(filename)
896 }
897
898 pub fn list(&self) -> Vec<String> {
900 self.pool.name2file.borrow().keys().cloned().collect()
901 }
902
903 pub fn count(&self) -> usize {
905 self.pool.name2file.borrow().len()
906 }
907}
908
909pub 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}