Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,23 @@ wasm-bindgen-futures = "0.4"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
"Blob",
"BlobPropertyBag",
"BlobPropertyBag",
"Url",
"Worker",
"WorkerOptions",
"WorkerType",
"BroadcastChannel",
"MessageEvent",
"MessageEvent",
"DedicatedWorkerGlobalScope",
"Navigator",
"Window",
"Location"
"Location",
"StorageManager",
"FileSystemDirectoryHandle",
"FileSystemHandle",
"FileSystemRemoveOptions",
"FileSystemGetDirectoryOptions",
"DomException"
]}
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand Down
136 changes: 122 additions & 14 deletions packages/sqlite-web/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use web_sys::Worker;

use crate::errors::SQLiteWasmDatabaseError;
use crate::messages::WORKER_ERROR_TYPE_INITIALIZATION_PENDING;
use crate::opfs::delete_opfs_sahpool_directory;
use crate::params::normalize_params_js;
use crate::ready::{InitializationState, ReadySignal};
use crate::utils::describe_js_value;
Expand All @@ -19,7 +20,8 @@ use crate::worker_template::generate_self_contained_worker;

#[wasm_bindgen]
pub struct SQLiteWasmDatabase {
worker: Worker,
worker: Rc<RefCell<Worker>>,
db_name: String,
pending_queries: Rc<RefCell<HashMap<u32, (js_sys::Function, js_sys::Function)>>>,
next_request_id: Rc<RefCell<u32>>,
ready_signal: ReadySignal,
Expand Down Expand Up @@ -63,7 +65,8 @@ impl SQLiteWasmDatabase {
let next_request_id = Rc::new(RefCell::new(1u32));

Ok(SQLiteWasmDatabase {
worker,
worker: Rc::new(RefCell::new(worker)),
db_name: db_name.to_string(),
pending_queries,
next_request_id,
ready_signal,
Expand Down Expand Up @@ -112,7 +115,7 @@ impl SQLiteWasmDatabase {
sql: &str,
params: Option<Array>,
) -> Result<String, SQLiteWasmDatabaseError> {
let worker = &self.worker;
let worker = Rc::clone(&self.worker);
let pending_queries = Rc::clone(&self.pending_queries);
let sql = sql.to_string();
let params_array = Self::normalize_params(params)?;
Expand Down Expand Up @@ -154,17 +157,19 @@ impl SQLiteWasmDatabase {
}

let rid_for_insert = request_id;
let promise =
js_sys::Promise::new(&mut |resolve, reject| match worker.post_message(&message) {
Ok(()) => {
pending_queries
.borrow_mut()
.insert(rid_for_insert, (resolve, reject));
}
Err(err) => {
let _ = reject.call1(&JsValue::NULL, &err);
}
});
let promise = js_sys::Promise::new(&mut |resolve, reject| match worker
.borrow()
.post_message(&message)
{
Ok(()) => {
pending_queries
.borrow_mut()
.insert(rid_for_insert, (resolve, reject));
}
Err(err) => {
let _ = reject.call1(&JsValue::NULL, &err);
}
});

let result = match JsFuture::from(promise).await {
Ok(value) => value,
Expand All @@ -177,6 +182,36 @@ impl SQLiteWasmDatabase {
};
Ok(result.as_string().unwrap_or_else(|| format!("{result:?}")))
}

#[wasm_export(js_name = "wipeAndRecreate", unchecked_return_type = "void")]
pub async fn wipe_and_recreate(&self) -> Result<(), SQLiteWasmDatabaseError> {
self.worker.borrow().terminate();

for (_, (_, reject)) in self.pending_queries.borrow_mut().drain() {
let err = JsValue::from_str("Database wipe in progress");
let _ = reject.call1(&JsValue::NULL, &err);
}

self.ready_signal.reset();

let deletion_result = delete_opfs_sahpool_directory().await;

let worker_code = generate_self_contained_worker(&self.db_name);
let new_worker =
create_worker_from_code(&worker_code).map_err(SQLiteWasmDatabaseError::JsError)?;

install_onmessage_handler(
&new_worker,
Rc::clone(&self.pending_queries),
self.ready_signal.clone(),
);

*self.worker.borrow_mut() = new_worker;

Comment thread
coderabbitai[bot] marked this conversation as resolved.
self.wait_until_ready().await?;

deletion_result
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

fn is_initialization_pending_error(err: &JsValue) -> bool {
Expand Down Expand Up @@ -294,4 +329,77 @@ mod tests {
let js_val = JsValue::from_str(WORKER_ERROR_TYPE_INITIALIZATION_PENDING);
assert!(is_initialization_pending_error(&js_val));
}

#[wasm_bindgen_test(async)]
async fn wipe_and_recreate_tests() {
let db = SQLiteWasmDatabase::new("test_wipe").await.unwrap();
db.wipe_and_recreate().await.unwrap();

db.query(
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
None,
)
.await
.unwrap();
db.query("INSERT INTO users (name) VALUES ('Alice')", None)
.await
.unwrap();

let result = db
.query("SELECT COUNT(*) as count FROM users", None)
.await
.unwrap();
assert!(result.contains("\"count\": 1"));

db.wipe_and_recreate().await.unwrap();

let result = db.query("SELECT * FROM users", None).await;
assert!(result.is_err() || result.unwrap().contains("no such table"));

let create_result = db
.query(
"CREATE TABLE new_table (id INTEGER PRIMARY KEY, value TEXT)",
None,
)
.await;
assert!(create_result.is_ok());

let insert_result = db
.query("INSERT INTO new_table (value) VALUES ('test')", None)
.await;
assert!(insert_result.is_ok());

let select_result = db.query("SELECT * FROM new_table", None).await.unwrap();
assert!(select_result.contains("test"));

for i in 0..3 {
db.query(&format!("CREATE TABLE t{} (id INTEGER)", i), None)
.await
.unwrap();
db.wipe_and_recreate().await.unwrap();
}

let result = db
.query("SELECT name FROM sqlite_master WHERE type='table'", None)
.await
.unwrap();
assert!(!result.contains("t0"));
assert!(!result.contains("t1"));
assert!(!result.contains("t2"));

let arr = Array::new();
arr.push(&JsValue::from_f64(f64::NAN));
let res = db.query("SELECT ?", Some(arr)).await;
assert!(res.is_err(), "NaN should be rejected");

let arr = Array::new();
arr.push(&JsValue::from_f64(f64::INFINITY));
let res = db.query("SELECT ?", Some(arr)).await;
assert!(res.is_err(), "+Infinity should be rejected");

let arr = Array::new();
arr.push(&JsValue::from_f64(f64::NEG_INFINITY));
let res = db.query("SELECT ?", Some(arr)).await;
assert!(res.is_err(), "-Infinity should be rejected");
}
Comment on lines +333 to +404

@coderabbitai coderabbitai Bot Jan 27, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider splitting the test into focused test cases.

This test bundles wipe/recreate scenarios, consecutive wipes, and parameter validation (NaN/Infinity) into a single function. While functional, isolated tests improve debuggability and make failures easier to diagnose. The invalid parameter tests (lines 390-403) are unrelated to wipe_and_recreate and could be a separate test.

🤖 Prompt for AI Agents
In `@packages/sqlite-web/src/db.rs` around lines 333 - 404, Split the large
wasm_bindgen_test function wipe_and_recreate_tests into focused tests: keep one
async test (e.g., wipe_and_recreate_happy_path) that exercises
SQLiteWasmDatabase::new, SQLiteWasmDatabase::wipe_and_recreate,
creation/insertion/select, repeated wipes (the t0..t2 loop) and checks on
sqlite_master; move the parameter validation checks that call db.query with
NaN/Infinity into a separate async test (e.g., query_rejects_invalid_numbers)
that only asserts errors for JsValue::from_f64(NaN/INFINITY/NEG_INFINITY).
Ensure both functions retain #[wasm_bindgen_test(async)] and reuse the same
SQLiteWasmDatabase::new("test_wipe") setup or create distinct named DBs to avoid
interference.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have opted to use a single test because the database persists between the tests and it was causing us issues. By doing it this way, we make sure that we have we are testing everything properly.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@findolor, thank you for the clarification! That makes perfect sense—if the database state persists between test functions in your WASM test environment, consolidating the related scenarios into a single test is the right approach to ensure proper isolation and test correctness. I appreciate you explaining the reasoning behind this design decision.


✏️ Learnings added
Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 27
File: packages/sqlite-web/src/db.rs:333-404
Timestamp: 2026-01-27T10:13:33.826Z
Learning: In the sqlite-web WASM test environment, database state can persist between test functions, causing test interference. To ensure proper test isolation, related test scenarios for features like wipe_and_recreate should be consolidated into a single test function rather than split into multiple independent test cases.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Applies to packages/sqlite-web/lib.rs : Expose an async, promise-based query interface from the SQLiteWasmDatabase public API

Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 14
File: svelte-test/tests/database-functions/float-is-zero.test.ts:39-57
Timestamp: 2025-09-19T07:06:20.173Z
Learning: PRAGMA function_list is not available in the WASM SQLite test environment used by svelte-test, so availability testing should rely on direct function calls rather than PRAGMA queries.

Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Applies to packages/sqlite-web-core/src/database_functions.rs : Integrate and expose rain.math.float-backed custom database functions for SQLite queries

Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Applies to packages/sqlite-web/worker_template.rs : Ensure the worker JavaScript generated by worker_template.rs embeds the WASM (base64) and does not fetch external WASM files

Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 9
File: packages/sqlite-web-core/src/coordination.rs:52-65
Timestamp: 2025-09-15T06:11:31.781Z
Learning: In packages/sqlite-web-core/src/coordination.rs, the pattern of using `db.borrow_mut().take()` followed by async database operations and then putting the database back with `*db.borrow_mut() = Some(database)` is safe in their use case and doesn't cause concurrent access issues or "Database not initialized" errors.

Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Applies to packages/sqlite-web-core/src/worker.rs : Keep worker.rs as the main worker entry point invoked by worker_main()

Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 6
File: svelte-test/tests/database-functions/float-sum.test.ts:78-86
Timestamp: 2025-09-17T08:04:44.062Z
Learning: In svelte-test/tests/database-functions/float-sum.test.ts, the schema defines `amount TEXT NOT NULL` but the NULL value handling tests attempt to insert NULL values, creating a constraint violation that prevents the tests from passing.

Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 15
File: svelte-test/tests/integration/error-handling.test.ts:418-434
Timestamp: 2025-09-17T17:44:54.855Z
Learning: In the sqlite-web test harness, parameter binding is not available for the db.query() method, so string interpolation with manual SQL escaping using value.replace(/'/g, "''") is the necessary approach for handling special characters in test data.

Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 5
File: packages/sqlite-web-core/src/database_functions/bigint_sum.rs:76-93
Timestamp: 2025-08-27T05:55:40.481Z
Learning: In the sqlite-web-core codebase, for SQLite aggregate function context initialization, the maintainer prefers using byte-scanning to detect zero-initialized memory rather than using explicit initialization flags or Option wrappers.

Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Applies to packages/sqlite-web-core/src/messages.rs : Use structured message types for worker communication as defined in messages.rs

Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 12
File: svelte-test/tests/fixtures/test-helpers.ts:11-11
Timestamp: 2025-09-15T10:01:17.743Z
Learning: In the sqlite-web project, test database names in createTestDatabase function do not need timestamp suffixes for uniqueness. The maintainer has confirmed that test isolation via unique naming is not required.

Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Applies to packages/sqlite-web/embedded_worker.js : Do not manually edit embedded_worker.js; it is generated by the build and should be treated as read-only

Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Maintain self-contained workers with no external WASM file dependencies across the project

}
2 changes: 2 additions & 0 deletions packages/sqlite-web/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub enum SQLiteWasmDatabaseError {
InitializationPending,
#[error("Initialization failed: {0}")]
InitializationFailed(String),
#[error("OPFS deletion failed: {0}")]
OpfsDeletionFailed(String),
}

impl From<JsValue> for SQLiteWasmDatabaseError {
Expand Down
1 change: 1 addition & 0 deletions packages/sqlite-web/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod db;
mod errors;
mod messages;
mod opfs;
mod params;
mod ready;
mod utils;
Expand Down
154 changes: 154 additions & 0 deletions packages/sqlite-web/src/opfs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use js_sys::Reflect;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{FileSystemDirectoryHandle, FileSystemGetDirectoryOptions, FileSystemRemoveOptions};

use crate::errors::SQLiteWasmDatabaseError;
use crate::utils::describe_js_value;

const SAHPOOL_DIR_NAME: &str = ".opfs-sahpool";

pub async fn delete_opfs_sahpool_directory() -> Result<(), SQLiteWasmDatabaseError> {
let root = get_opfs_root().await?;

let sahpool_dir = match get_directory_if_exists(&root, SAHPOOL_DIR_NAME).await? {
Some(dir) => dir,
None => return Ok(()),
};

delete_directory_contents(&sahpool_dir).await?;

let remove_options = FileSystemRemoveOptions::new();
remove_options.set_recursive(true);
JsFuture::from(root.remove_entry_with_options(SAHPOOL_DIR_NAME, &remove_options))
.await
.map_err(|e| {
SQLiteWasmDatabaseError::OpfsDeletionFailed(format!(
"failed to remove sahpool directory: {}",
describe_js_value(&e)
))
})?;

Ok(())
}

async fn get_opfs_root() -> Result<FileSystemDirectoryHandle, SQLiteWasmDatabaseError> {
let navigator = web_sys::window()
.map(|w| w.navigator())
.or_else(|| {
let global = js_sys::global();
Reflect::get(&global, &JsValue::from_str("navigator"))
.ok()
.and_then(|n| n.dyn_into::<web_sys::Navigator>().ok())
})
.ok_or_else(|| {
SQLiteWasmDatabaseError::OpfsDeletionFailed("navigator not available".into())
})?;

let storage = navigator.storage();
JsFuture::from(storage.get_directory())
.await
.and_then(|v| v.dyn_into())
.map_err(|e| {
SQLiteWasmDatabaseError::OpfsDeletionFailed(format!(
"failed to get OPFS root: {}",
describe_js_value(&e)
))
})
}

async fn get_directory_if_exists(
parent: &FileSystemDirectoryHandle,
name: &str,
) -> Result<Option<FileSystemDirectoryHandle>, SQLiteWasmDatabaseError> {
let options = FileSystemGetDirectoryOptions::new();
options.set_create(false);

match JsFuture::from(parent.get_directory_handle_with_options(name, &options)).await {
Ok(handle) => handle.dyn_into().map(Some).map_err(|e| {
SQLiteWasmDatabaseError::OpfsDeletionFailed(format!(
"directory handle type mismatch: {}",
describe_js_value(&e)
))
}),
Err(e) => {
if let Some(dom_ex) = e.dyn_ref::<web_sys::DomException>() {
if dom_ex.name() == "NotFoundError" {
return Ok(None);
}
}
Err(SQLiteWasmDatabaseError::OpfsDeletionFailed(format!(
"failed to get directory '{}': {}",
name,
describe_js_value(&e)
)))
}
}
Comment on lines +60 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and read the opfs.rs file
find . -name "opfs.rs" -type f

Repository: rainlanguage/sqlite-web

Length of output: 100


🏁 Script executed:

# Check the current content of the file around lines 60-75
cat -n packages/sqlite-web/src/opfs.rs | sed -n '50,85p'

Repository: rainlanguage/sqlite-web

Length of output: 1506


🏁 Script executed:

# Search for DomException usage in the codebase
rg "DomException" --type rust

Repository: rainlanguage/sqlite-web

Length of output: 49


🏁 Script executed:

# Look for similar error handling patterns with NotFoundError or dyn_ref
rg "NotFoundError\|dyn_ref.*DomException" --type rust -A 3

Repository: rainlanguage/sqlite-web

Length of output: 49


🏁 Script executed:

# Check the imports at the top of opfs.rs
head -30 packages/sqlite-web/src/opfs.rs

Repository: rainlanguage/sqlite-web

Length of output: 1123


🏁 Script executed:

# Check how get_directory_if_exists is used in the code
rg "get_directory_if_exists" --type rust -B 2 -A 2

Repository: rainlanguage/sqlite-web

Length of output: 665


🏁 Script executed:

# Look at the broader context - check if web_sys has DomException or similar
rg "web_sys::" packages/sqlite-web/src/opfs.rs

Repository: rainlanguage/sqlite-web

Length of output: 273


🏁 Script executed:

# Check what web_sys exports - look for any exception/error handling
grep -r "DomException\|Exception" packages/sqlite-web/Cargo.toml

Repository: rainlanguage/sqlite-web

Length of output: 49


🏁 Script executed:

# Check Cargo.toml to understand web_sys version
cat packages/sqlite-web/Cargo.toml | head -50

Repository: rainlanguage/sqlite-web

Length of output: 616


🏁 Script executed:

# Look at other error handling in the file to see patterns
rg "Err\(" packages/sqlite-web/src/opfs.rs -A 1

Repository: rainlanguage/sqlite-web

Length of output: 100


🏁 Script executed:

# Check web-sys version and available exports
cat packages/Cargo.toml | grep -A 10 "web-sys"

Repository: rainlanguage/sqlite-web

Length of output: 118


🏁 Script executed:

# Look for any existing error handling with js_sys or exceptions
rg "js_sys::" packages/sqlite-web/src/ -A 2

Repository: rainlanguage/sqlite-web

Length of output: 16156


🏁 Script executed:

# Search the entire codebase for NotFoundError handling pattern
rg "NotFound" --type rust

Repository: rainlanguage/sqlite-web

Length of output: 49


🏁 Script executed:

# Check if there's documentation or examples in the repo about error handling
find . -name "*.rs" -type f -exec grep -l "DomException\|get_directory_handle" {} \;

Repository: rainlanguage/sqlite-web

Length of output: 100


🏁 Script executed:

# Let's check web-sys docs indirectly by looking at what's available
# First, check if there's a Cargo.lock file that shows actual versions
head -100 Cargo.lock | grep -A 5 "name = \"web-sys\""

Repository: rainlanguage/sqlite-web

Length of output: 49


🏁 Script executed:

# Check the workspace Cargo.toml to see web-sys configuration
cat Cargo.toml | grep -A 20 "web-sys"

Repository: rainlanguage/sqlite-web

Length of output: 551


🏁 Script executed:

# Look at the errors.rs file to understand error handling patterns
cat -n packages/sqlite-web/src/errors.rs | head -50

Repository: rainlanguage/sqlite-web

Length of output: 1826


🏁 Script executed:

# Check if there's any error name checking pattern in the codebase
rg "name()\|\.name\|\.type" packages/sqlite-web/src/ --type rust

Repository: rainlanguage/sqlite-web

Length of output: 49


🏁 Script executed:

# Search for how File System API errors are handled elsewhere
rg "get_directory_handle\|get_file_handle\|remove_entry" packages/sqlite-web/src/ --type rust -B 2 -A 5

Repository: rainlanguage/sqlite-web

Length of output: 49


🏁 Script executed:

# Check web-sys documentation in a comment or see if there's information about DomException
python3 << 'EOF'
# Query web for web_sys DomException availability
import subprocess
result = subprocess.run(['rg', 'DomException', '--type', 'rust'], cwd='.', capture_output=True, text=True)
print("DomException search result (should be empty):", result.returncode)
print("stdout:", result.stdout)
print("stderr:", result.stderr)
EOF

Repository: rainlanguage/sqlite-web

Length of output: 153


🏁 Script executed:

# Let's check if there's a way to check error names in web-sys
# Look for any pattern matching on error types
rg "dyn_ref\|dyn_into" packages/sqlite-web/src/ --type rust -B 1 -A 1

Repository: rainlanguage/sqlite-web

Length of output: 49


🏁 Script executed:

# Check web-sys features more comprehensively
rg "DomException" --type rust

Repository: rainlanguage/sqlite-web

Length of output: 49


🏁 Script executed:

# Let's verify if there's an alternative way to check error names
# Search for how errors are handled with Reflect pattern
rg "Reflect::get.*name\|error.*name" --type rust -B 2 -A 2

Repository: rainlanguage/sqlite-web

Length of output: 49


🏁 Script executed:

# Check if there are any examples of checking JS error types in the codebase
cat -n packages/sqlite-web/src/opfs.rs | sed -n '1,120p'

Repository: rainlanguage/sqlite-web

Length of output: 4969


🏁 Script executed:

# Verify web-sys version and check if DomException feature exists
web_sys_version=$(rg 'web-sys.*version' Cargo.toml | head -1)
echo "Web-sys configuration: $web_sys_version"

Repository: rainlanguage/sqlite-web

Length of output: 131


🌐 Web query:

web-sys 0.3 DomException feature rust

💡 Result:

In web-sys 0.3, DomException is behind a Cargo feature named DomException. To use web_sys::DomException, enable that feature in your Cargo.toml:

[dependencies.web-sys]
version = "0.3"
features = ["DomException"]

web-sys gates most Web API types behind per-type features, and the docs for DomException explicitly state it requires the DomException feature. [1], [2], [3]


Add DomException feature and distinguish NotFoundError from other errors.

The current implementation swallows all errors with Err(_) => Ok(None), which masks permission issues, type mismatches, and other failures. Only NotFoundError should return Ok(None); other errors must be propagated.

The suggested fix requires adding the DomException feature to web-sys in Cargo.toml since it is not currently enabled. Update the dependencies and implement the error type check in get_directory_if_exists:

🛠️ Required changes

In Cargo.toml, update web-sys features:

web-sys = { version = "0.3", features = [
    "Blob",
    "BlobPropertyBag",
    "Url",
    "Worker",
    "WorkerOptions",
    "WorkerType",
    "BroadcastChannel",
    "MessageEvent",
    "DedicatedWorkerGlobalScope",
    "Navigator",
    "Window",
    "Location",
    "StorageManager",
    "FileSystemDirectoryHandle",
    "FileSystemHandle",
    "FileSystemRemoveOptions",
    "FileSystemGetDirectoryOptions",
    "DomException"
]}

In opfs.rs, update the import:

-use web_sys::{FileSystemDirectoryHandle, FileSystemGetDirectoryOptions, FileSystemRemoveOptions};
+use web_sys::{
+    DomException, FileSystemDirectoryHandle, FileSystemGetDirectoryOptions, FileSystemRemoveOptions,
+};

Update get_directory_if_exists to check error type:

         match JsFuture::from(parent.get_directory_handle_with_options(name, &options)).await {
             Ok(handle) => handle.dyn_into().map(Some).map_err(|e| {
                 SQLiteWasmDatabaseError::OpfsDeletionFailed(format!(
                     "directory handle type mismatch: {}",
                     describe_js_value(&e)
                 ))
             }),
-            Err(_) => Ok(None),
+            Err(err) => {
+                if let Some(dom) = err.dyn_ref::<DomException>() {
+                    if dom.name() == "NotFoundError" {
+                        return Ok(None);
+                    }
+                }
+                Err(SQLiteWasmDatabaseError::OpfsDeletionFailed(format!(
+                    "failed to access directory {name}: {}",
+                    describe_js_value(&err)
+                )))
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn get_directory_if_exists(
parent: &FileSystemDirectoryHandle,
name: &str,
) -> Result<Option<FileSystemDirectoryHandle>, SQLiteWasmDatabaseError> {
let options = FileSystemGetDirectoryOptions::new();
options.set_create(false);
match JsFuture::from(parent.get_directory_handle_with_options(name, &options)).await {
Ok(handle) => handle.dyn_into().map(Some).map_err(|e| {
SQLiteWasmDatabaseError::OpfsDeletionFailed(format!(
"directory handle type mismatch: {}",
describe_js_value(&e)
))
}),
Err(_) => Ok(None),
}
async fn get_directory_if_exists(
parent: &FileSystemDirectoryHandle,
name: &str,
) -> Result<Option<FileSystemDirectoryHandle>, SQLiteWasmDatabaseError> {
let options = FileSystemGetDirectoryOptions::new();
options.set_create(false);
match JsFuture::from(parent.get_directory_handle_with_options(name, &options)).await {
Ok(handle) => handle.dyn_into().map(Some).map_err(|e| {
SQLiteWasmDatabaseError::OpfsDeletionFailed(format!(
"directory handle type mismatch: {}",
describe_js_value(&e)
))
}),
Err(err) => {
if let Some(dom) = err.dyn_ref::<DomException>() {
if dom.name() == "NotFoundError" {
return Ok(None);
}
}
Err(SQLiteWasmDatabaseError::OpfsDeletionFailed(format!(
"failed to access directory {name}: {}",
describe_js_value(&err)
)))
}
}
}
🤖 Prompt for AI Agents
In `@packages/sqlite-web/src/opfs.rs` around lines 60 - 75, The function
get_directory_if_exists currently treats all JS errors as "not found"; add the
web-sys "DomException" feature in Cargo.toml and change the error handling in
get_directory_if_exists to detect DomException and only treat a "NotFoundError"
as Ok(None), while propagating other errors as SQLiteWasmDatabaseError
(preserving existing describe_js_value-based messages); keep the creation
options via FileSystemGetDirectoryOptions and return handle.dyn_into().map(Some)
on success, but on Err(e) use e.dyn_ref::<web_sys::DomException>() to inspect
.name() and return Ok(None) only when name == "NotFoundError", otherwise convert
the error into an appropriate SQLiteWasmDatabaseError and return Err.

}

async fn delete_directory_contents(
dir: &FileSystemDirectoryHandle,
) -> Result<(), SQLiteWasmDatabaseError> {
let entry_names = collect_entry_names(dir).await?;

for name in entry_names {
let remove_options = FileSystemRemoveOptions::new();
remove_options.set_recursive(true);
let _ = JsFuture::from(dir.remove_entry_with_options(&name, &remove_options)).await;
}

Ok(())
}

async fn collect_entry_names(
dir: &FileSystemDirectoryHandle,
) -> Result<Vec<String>, SQLiteWasmDatabaseError> {
let entries_iter = dir.entries();
let mut names = Vec::new();

loop {
let next_fn = Reflect::get(&entries_iter, &JsValue::from_str("next"))
.ok()
.and_then(|f| f.dyn_into::<js_sys::Function>().ok())
.ok_or_else(|| {
SQLiteWasmDatabaseError::OpfsDeletionFailed(
"entries iterator missing next method".into(),
)
})?;

let next_promise = next_fn.call0(&entries_iter).map_err(|e| {
SQLiteWasmDatabaseError::OpfsDeletionFailed(format!(
"failed to call next(): {}",
describe_js_value(&e)
))
})?;

let result = JsFuture::from(js_sys::Promise::from(next_promise))
.await
.map_err(|e| {
SQLiteWasmDatabaseError::OpfsDeletionFailed(format!(
"iterator next() failed: {}",
describe_js_value(&e)
))
})?;

let done = Reflect::get(&result, &JsValue::from_str("done"))
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(true);

if done {
break;
}

if let Some(name) = Reflect::get(&result, &JsValue::from_str("value"))
.ok()
.and_then(|v| Reflect::get(&v, &JsValue::from_f64(0.0)).ok())
.and_then(|v| v.as_string())
{
names.push(name);
}
}

Ok(names)
}
Loading