From 645fabe2eb9cfcea3da534d0d2d3ae7b932aee6a Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 10 Nov 2025 12:45:08 +0100 Subject: [PATCH 01/15] Update Cargo.lock --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1c43471..27c39c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4070,7 +4070,7 @@ dependencies = [ [[package]] name = "sqlite-web" -version = "0.0.1-alpha.6" +version = "0.0.1-alpha.7" dependencies = [ "base64 0.21.7", "js-sys", @@ -4086,7 +4086,7 @@ dependencies = [ [[package]] name = "sqlite-web-core" -version = "0.0.1-alpha.6" +version = "0.0.1-alpha.7" dependencies = [ "alloy", "base64 0.21.7", From 9ef45e21622ea80d6280d719518c954ecad63010 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 10 Nov 2025 12:58:33 +0100 Subject: [PATCH 02/15] Initial pass for the ready function --- packages/sqlite-web-core/src/coordination.rs | 78 +++++++- packages/sqlite-web/src/lib.rs | 192 ++++++++++++++++++- packages/sqlite-web/src/worker_template.rs | 5 +- 3 files changed, 263 insertions(+), 12 deletions(-) diff --git a/packages/sqlite-web-core/src/coordination.rs b/packages/sqlite-web-core/src/coordination.rs index 10fd273..5e094d6 100644 --- a/packages/sqlite-web-core/src/coordination.rs +++ b/packages/sqlite-web-core/src/coordination.rs @@ -6,7 +6,7 @@ use uuid::Uuid; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_futures::spawn_local; -use web_sys::BroadcastChannel; +use web_sys::{BroadcastChannel, DedicatedWorkerGlobalScope}; use crate::database::SQLiteDatabase; use crate::messages::{ChannelMessage, PendingQuery}; @@ -16,10 +16,12 @@ use crate::util::{js_value_to_string, sanitize_identifier, set_js_property}; pub struct WorkerState { pub worker_id: String, pub is_leader: Rc>, + pub has_leader: Rc>, pub db: Rc>>, pub channel: BroadcastChannel, pub db_name: String, pub pending_queries: Rc>>, + pub follower_timeout_ms: f64, } fn reflect_get(target: &JsValue, key: &str) -> Result { @@ -40,6 +42,32 @@ fn send_channel_message( }) } +fn post_worker_message(obj: &js_sys::Object) -> Result<(), String> { + let global = js_sys::global(); + let scope: DedicatedWorkerGlobalScope = global + .dyn_into() + .map_err(|_| "Failed to access worker scope".to_string())?; + scope + .post_message(obj.as_ref()) + .map_err(|err| js_value_to_string(&err)) +} + +fn send_worker_ready_message() -> Result<(), String> { + let message = js_sys::Object::new(); + set_js_property(&message, "type", &JsValue::from_str("worker-ready")) + .map_err(|err| js_value_to_string(&err))?; + post_worker_message(&message) +} + +fn send_worker_error_message(error: &str) -> Result<(), String> { + let message = js_sys::Object::new(); + set_js_property(&message, "type", &JsValue::from_str("worker-error")) + .map_err(|err| js_value_to_string(&err))?; + set_js_property(&message, "error", &JsValue::from_str(error)) + .map_err(|err| js_value_to_string(&err))?; + post_worker_message(&message) +} + impl WorkerState { pub fn new() -> Result { fn get_db_name_from_global() -> Result { @@ -62,23 +90,39 @@ impl WorkerState { } } + fn get_follower_timeout_from_global() -> f64 { + let global = js_sys::global(); + let val = Reflect::get(&global, &JsValue::from_str("__SQLITE_FOLLOWER_TIMEOUT_MS")) + .unwrap_or(JsValue::UNDEFINED); + if let Some(n) = val.as_f64() { + if n.is_finite() && n >= 0.0 { + return n; + } + } + 5000.0 + } + let worker_id = Uuid::new_v4().to_string(); let db_name_raw = get_db_name_from_global()?; let channel_name = format!("sqlite-queries-{}", sanitize_identifier(&db_name_raw)); let channel = BroadcastChannel::new(&channel_name)?; + let follower_timeout_ms = get_follower_timeout_from_global(); Ok(WorkerState { worker_id, is_leader: Rc::new(RefCell::new(false)), + has_leader: Rc::new(RefCell::new(false)), db: Rc::new(RefCell::new(None)), channel, db_name: db_name_raw, pending_queries: Rc::new(RefCell::new(HashMap::new())), + follower_timeout_ms, }) } pub fn setup_channel_listener(&self) -> Result<(), JsValue> { let is_leader = Rc::clone(&self.is_leader); + let has_leader = Rc::clone(&self.has_leader); let db = Rc::clone(&self.db); let pending_queries = Rc::clone(&self.pending_queries); let channel = self.channel.clone(); @@ -86,7 +130,14 @@ impl WorkerState { let onmessage = Closure::wrap(Box::new(move |event: web_sys::MessageEvent| { let data = event.data(); if let Ok(msg) = serde_wasm_bindgen::from_value::(data) { - handle_channel_message(&is_leader, &db, &channel, &pending_queries, msg); + handle_channel_message( + &is_leader, + &has_leader, + &db, + &channel, + &pending_queries, + msg, + ); } }) as Box); @@ -99,6 +150,7 @@ impl WorkerState { pub async fn attempt_leadership(&self) -> Result<(), JsValue> { let worker_id = self.worker_id.clone(); let is_leader = Rc::clone(&self.is_leader); + let has_leader = Rc::clone(&self.has_leader); let db = Rc::clone(&self.db); let channel = self.channel.clone(); let db_name_for_handler = self.db_name.clone(); @@ -113,16 +165,19 @@ impl WorkerState { let handler = Closure::once(move |_lock: JsValue| -> Promise { *is_leader.borrow_mut() = true; + *has_leader.borrow_mut() = true; let db = Rc::clone(&db); let channel = channel.clone(); let worker_id = worker_id.clone(); let db_name = db_name_for_handler.clone(); + let has_leader_inner = Rc::clone(&has_leader); spawn_local(async move { match SQLiteDatabase::initialize_opfs(&db_name).await { Ok(database) => { *db.borrow_mut() = Some(database); + *has_leader_inner.borrow_mut() = true; let msg = ChannelMessage::NewLeader { leader_id: worker_id.clone(), @@ -135,8 +190,15 @@ impl WorkerState { }; let _ = send_channel_message(&channel, &fallback); } + if let Err(err_msg) = send_worker_ready_message() { + let _ = send_worker_error_message(&err_msg); + } + } + Err(err) => { + let msg = js_value_to_string(&err); + *has_leader_inner.borrow_mut() = false; + let _ = send_worker_error_message(&msg); } - Err(_e) => {} } }); @@ -169,6 +231,9 @@ impl WorkerState { if *self.is_leader.borrow() { exec_on_db(Rc::clone(&self.db), sql, params).await } else { + if !*self.has_leader.borrow() { + return Err("InitializationPending".to_string()); + } let query_id = Uuid::new_v4().to_string(); let promise = Promise::new(&mut |resolve, reject| { @@ -182,7 +247,7 @@ impl WorkerState { let timeout_promise = schedule_timeout_promise( Rc::clone(&self.pending_queries), query_id.clone(), - 5000.0, + self.follower_timeout_ms, ); let result = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::race( @@ -202,6 +267,7 @@ impl WorkerState { fn handle_channel_message( is_leader: &Rc>, + has_leader: &Rc>, db: &Rc>>, channel: &BroadcastChannel, pending_queries: &Rc>>, @@ -235,7 +301,9 @@ fn handle_channel_message( result, error, } => handle_query_response(pending_queries, query_id, result, error), - ChannelMessage::NewLeader { leader_id: _ } => {} + ChannelMessage::NewLeader { leader_id: _ } => { + *has_leader.borrow_mut() = true; + } } } diff --git a/packages/sqlite-web/src/lib.rs b/packages/sqlite-web/src/lib.rs index 13074e7..931874d 100644 --- a/packages/sqlite-web/src/lib.rs +++ b/packages/sqlite-web/src/lib.rs @@ -30,11 +30,25 @@ fn create_worker_from_code(worker_code: &str) -> Result { fn install_onmessage_handler( worker: &Worker, pending_queries: Rc>>, + ready_state: Rc>, + ready_resolve: Rc>>, + ready_reject: Rc>>, + ready_promise: Rc>>, ) { let pending_queries_clone = Rc::clone(&pending_queries); + let ready_state_clone = Rc::clone(&ready_state); + let ready_resolve_clone = Rc::clone(&ready_resolve); + let ready_reject_clone = Rc::clone(&ready_reject); + let ready_promise_clone = Rc::clone(&ready_promise); let onmessage = Closure::wrap(Box::new(move |event: MessageEvent| { let data = event.data(); - if handle_worker_control_message(&data) { + if handle_worker_control_message( + &data, + &ready_state_clone, + &ready_resolve_clone, + &ready_reject_clone, + &ready_promise_clone, + ) { return; } handle_query_result_message(&data, &pending_queries_clone); @@ -44,13 +58,46 @@ fn install_onmessage_handler( onmessage.forget(); } -fn handle_worker_control_message(data: &JsValue) -> bool { +fn handle_worker_control_message( + data: &JsValue, + ready_state: &Rc>, + ready_resolve: &Rc>>, + ready_reject: &Rc>>, + ready_promise: &Rc>>, +) -> bool { if let Ok(obj) = js_sys::Reflect::get(data, &JsValue::from_str("type")) { if let Some(msg_type) = obj.as_string() { if msg_type == "worker-ready" { + { + let mut state = ready_state.borrow_mut(); + if matches!(*state, InitializationState::Ready) { + return true; + } + *state = InitializationState::Ready; + } + if let Some(resolve) = ready_resolve.borrow_mut().take() { + let _ = resolve.call0(&JsValue::NULL); + } + ready_reject.borrow_mut().take(); + ready_promise.borrow_mut().take(); return true; } else if msg_type == "worker-error" { - let _ = js_sys::Reflect::get(data, &JsValue::from_str("error")); + let error_value = js_sys::Reflect::get(data, &JsValue::from_str("error")) + .ok() + .unwrap_or(JsValue::from_str("Unknown worker error")); + let error_text = describe_js_value(&error_value); + { + let mut state = ready_state.borrow_mut(); + if !matches!(&*state, InitializationState::Failed(existing) if existing == &error_text) + { + *state = InitializationState::Failed(error_text.clone()); + } + } + ready_resolve.borrow_mut().take(); + if let Some(reject) = ready_reject.borrow_mut().take() { + let _ = reject.call1(&JsValue::NULL, &JsValue::from_str(&error_text)); + } + ready_promise.borrow_mut().take(); return true; } } @@ -165,11 +212,22 @@ fn normalize_one_param(v: &JsValue, index: u32) -> Result>>, next_request_id: Rc>, + ready_state: Rc>, + ready_promise: Rc>>, + _ready_resolve: Rc>>, + _ready_reject: Rc>>, } impl Serialize for SQLiteWasmDatabase { @@ -182,6 +240,31 @@ impl Serialize for SQLiteWasmDatabase { state.end() } } + +fn describe_js_value(value: &JsValue) -> String { + if let Some(s) = value.as_string() { + return s; + } + if let Some(n) = value.as_f64() { + if n.fract() == 0.0 { + return format!("{n:.0}"); + } + return format!("{n}"); + } + format!("{value:?}") +} + +fn create_ready_promise( + resolve_cell: &Rc>>, + reject_cell: &Rc>>, +) -> js_sys::Promise { + let resolve_clone = Rc::clone(resolve_cell); + let reject_clone = Rc::clone(reject_cell); + js_sys::Promise::new(&mut move |resolve, reject| { + resolve_clone.borrow_mut().replace(resolve); + reject_clone.borrow_mut().replace(reject); + }) +} impl<'de> Deserialize<'de> for SQLiteWasmDatabase { fn deserialize(deserializer: D) -> Result where @@ -200,7 +283,7 @@ impl<'de> Deserialize<'de> for SQLiteWasmDatabase { "dbName must be a non-empty string", )); } - Self::new(trimmed).map_err(|e| { + Self::construct(trimmed).map_err(|e| { serde::de::Error::custom(format!("Failed to create SQLiteWasmDatabase: {e:?}")) }) } @@ -212,6 +295,10 @@ pub enum SQLiteWasmDatabaseError { SerdeError(#[from] serde_wasm_bindgen::Error), #[error("JavaScript error: {0:?}")] JsError(JsValue), + #[error("Initialization pending")] + InitializationPending, + #[error("Initialization failed: {0}")] + InitializationFailed(String), } impl From for SQLiteWasmDatabaseError { @@ -257,18 +344,41 @@ impl SQLiteWasmDatabase { "Database name is required", ))); } + Self::construct(db_name) + } + + fn construct(db_name: &str) -> Result { let worker_code = generate_self_contained_worker(db_name); let worker = create_worker_from_code(&worker_code)?; let pending_queries: Rc>> = Rc::new(RefCell::new(HashMap::new())); - install_onmessage_handler(&worker, Rc::clone(&pending_queries)); + let ready_state = Rc::new(RefCell::new(InitializationState::Pending)); + let ready_resolve_cell = Rc::new(RefCell::new(None)); + let ready_reject_cell = Rc::new(RefCell::new(None)); + let ready_promise = Rc::new(RefCell::new(None)); + { + let promise = create_ready_promise(&ready_resolve_cell, &ready_reject_cell); + ready_promise.borrow_mut().replace(promise); + } + install_onmessage_handler( + &worker, + Rc::clone(&pending_queries), + Rc::clone(&ready_state), + Rc::clone(&ready_resolve_cell), + Rc::clone(&ready_reject_cell), + Rc::clone(&ready_promise), + ); let next_request_id = Rc::new(RefCell::new(1u32)); Ok(SQLiteWasmDatabase { worker, pending_queries, next_request_id, + ready_state, + ready_promise, + _ready_resolve: ready_resolve_cell, + _ready_reject: ready_reject_cell, }) } @@ -281,6 +391,47 @@ impl SQLiteWasmDatabase { }) } + #[wasm_export(js_name = "ready")] + pub async fn ready(&self) -> Result<(), SQLiteWasmDatabaseError> { + match &*self.ready_state.borrow() { + InitializationState::Ready => return Ok(()), + InitializationState::Failed(reason) => { + return Err(SQLiteWasmDatabaseError::InitializationFailed( + reason.clone(), + )); + } + InitializationState::Pending => {} + } + + let promise = self + .ready_promise + .borrow() + .as_ref() + .cloned() + .ok_or_else(|| { + SQLiteWasmDatabaseError::InitializationFailed( + "Worker readiness promise missing".to_string(), + ) + })?; + + match JsFuture::from(promise).await { + Ok(_) => match &*self.ready_state.borrow() { + InitializationState::Ready => Ok(()), + InitializationState::Failed(reason) => Err( + SQLiteWasmDatabaseError::InitializationFailed(reason.clone()), + ), + InitializationState::Pending => Err(SQLiteWasmDatabaseError::InitializationFailed( + "Worker failed to signal readiness".to_string(), + )), + }, + Err(err) => { + let reason = describe_js_value(&err); + *self.ready_state.borrow_mut() = InitializationState::Failed(reason.clone()); + Err(SQLiteWasmDatabaseError::InitializationFailed(reason)) + } + } + } + /// Execute a SQL query (optionally parameterized via JS Array) /// /// Passing `undefined`/`null` from JS maps to `None`. @@ -296,6 +447,18 @@ impl SQLiteWasmDatabase { let params_js = params.map(JsValue::from).unwrap_or(JsValue::UNDEFINED); let params_array = Self::normalize_params_js(¶ms_js)?; + match &*self.ready_state.borrow() { + InitializationState::Ready => {} + InitializationState::Pending => { + return Err(SQLiteWasmDatabaseError::InitializationPending); + } + InitializationState::Failed(reason) => { + return Err(SQLiteWasmDatabaseError::InitializationFailed( + reason.clone(), + )); + } + } + // Build the message object up-front and propagate errors let message = js_sys::Object::new(); js_sys::Reflect::set( @@ -344,7 +507,20 @@ impl SQLiteWasmDatabase { } }); - let result = JsFuture::from(promise).await?; + let result = match JsFuture::from(promise).await { + Ok(value) => value, + Err(err) => { + if err + .as_string() + .as_deref() + .map(|s| s == "InitializationPending") + .unwrap_or(false) + { + return Err(SQLiteWasmDatabaseError::InitializationPending); + } + return Err(SQLiteWasmDatabaseError::JsError(err)); + } + }; Ok(result.as_string().unwrap_or_else(|| format!("{result:?}"))) } } @@ -465,6 +641,10 @@ mod tests { || worker_code.contains("import") || worker_code.len() > 100 ); + assert!( + worker_code.contains("__SQLITE_FOLLOWER_TIMEOUT_MS"), + "Worker template should embed follower timeout configuration" + ); } // --- Test helpers for spying on Worker.prototype.postMessage --- diff --git a/packages/sqlite-web/src/worker_template.rs b/packages/sqlite-web/src/worker_template.rs index 4209abf..7268942 100644 --- a/packages/sqlite-web/src/worker_template.rs +++ b/packages/sqlite-web/src/worker_template.rs @@ -4,7 +4,10 @@ pub fn generate_self_contained_worker(db_name: &str) -> String { // Safely JSON-encode the db name for JS embedding let encoded = serde_json::to_string(db_name).unwrap_or_else(|_| "\"unknown\"".to_string()); - let prefix = format!("self.__SQLITE_DB_NAME = {};\n", encoded); + let prefix = format!( + "self.__SQLITE_DB_NAME = {};\nself.__SQLITE_FOLLOWER_TIMEOUT_MS = 5000.0;\n", + encoded + ); // Use the bundled worker template with embedded WASM let body = include_str!("embedded_worker.js"); format!("{}{}", prefix, body) From 6b3c5d1d550b8ab0169f0b4b33c6731fa798aaca Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 10 Nov 2025 13:18:23 +0100 Subject: [PATCH 03/15] Turn the constructor into an async function and wait for readiness there --- packages/sqlite-web-core/src/coordination.rs | 9 ++- packages/sqlite-web/src/lib.rs | 75 ++++++-------------- 2 files changed, 24 insertions(+), 60 deletions(-) diff --git a/packages/sqlite-web-core/src/coordination.rs b/packages/sqlite-web-core/src/coordination.rs index 5e094d6..a9815f9 100644 --- a/packages/sqlite-web-core/src/coordination.rs +++ b/packages/sqlite-web-core/src/coordination.rs @@ -569,12 +569,11 @@ mod tests { .execute_query("SELECT 1".to_string(), None) .await; match result { - Err(msg) => assert!( - msg.contains("timeout") || msg.contains("Query timeout"), - "Follower should timeout, got: {}", - msg + Err(msg) => assert_eq!( + msg, "InitializationPending", + "Follower should reject while leader is pending" ), - Ok(_) => panic!("Expected timeout error for follower"), + Ok(_) => panic!("Expected initialization error for follower"), } } } diff --git a/packages/sqlite-web/src/lib.rs b/packages/sqlite-web/src/lib.rs index 931874d..24106d2 100644 --- a/packages/sqlite-web/src/lib.rs +++ b/packages/sqlite-web/src/lib.rs @@ -1,6 +1,6 @@ use base64::Engine; use js_sys::Array; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; @@ -265,30 +265,6 @@ fn create_ready_promise( reject_clone.borrow_mut().replace(reject); }) } -impl<'de> Deserialize<'de> for SQLiteWasmDatabase { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct InitOpts { - #[serde(rename = "dbName")] - db_name: String, - } - - let opts = InitOpts::deserialize(deserializer)?; - let trimmed = opts.db_name.trim(); - if trimmed.is_empty() { - return Err(serde::de::Error::custom( - "dbName must be a non-empty string", - )); - } - Self::construct(trimmed).map_err(|e| { - serde::de::Error::custom(format!("Failed to create SQLiteWasmDatabase: {e:?}")) - }) - } -} - #[derive(Debug, Error)] pub enum SQLiteWasmDatabaseError { #[error(transparent)] @@ -337,14 +313,16 @@ impl SQLiteWasmDatabase { } /// Create a new database connection with fully embedded worker #[wasm_export(js_name = "new", preserve_js_class)] - pub fn new(db_name: &str) -> Result { + pub async fn new(db_name: &str) -> Result { let db_name = db_name.trim(); if db_name.is_empty() { return Err(SQLiteWasmDatabaseError::JsError(JsValue::from_str( "Database name is required", ))); } - Self::construct(db_name) + let db = Self::construct(db_name)?; + db.wait_until_ready().await?; + Ok(db) } fn construct(db_name: &str) -> Result { @@ -391,8 +369,7 @@ impl SQLiteWasmDatabase { }) } - #[wasm_export(js_name = "ready")] - pub async fn ready(&self) -> Result<(), SQLiteWasmDatabaseError> { + async fn wait_until_ready(&self) -> Result<(), SQLiteWasmDatabaseError> { match &*self.ready_state.borrow() { InitializationState::Ready => return Ok(()), InitializationState::Failed(reason) => { @@ -447,16 +424,10 @@ impl SQLiteWasmDatabase { let params_js = params.map(JsValue::from).unwrap_or(JsValue::UNDEFINED); let params_array = Self::normalize_params_js(¶ms_js)?; - match &*self.ready_state.borrow() { - InitializationState::Ready => {} - InitializationState::Pending => { - return Err(SQLiteWasmDatabaseError::InitializationPending); - } - InitializationState::Failed(reason) => { - return Err(SQLiteWasmDatabaseError::InitializationFailed( - reason.clone(), - )); - } + if let InitializationState::Failed(reason) = &*self.ready_state.borrow() { + return Err(SQLiteWasmDatabaseError::InitializationFailed( + reason.clone(), + )); } // Build the message object up-front and propagate errors @@ -572,8 +543,10 @@ mod tests { } #[wasm_bindgen_test] - fn test_sqlite_wasm_database_serialization() { - let db = SQLiteWasmDatabase::new("testdb").expect("Should create database"); + async fn test_sqlite_wasm_database_serialization() { + let db = SQLiteWasmDatabase::new("testdb") + .await + .expect("Should create database"); let serialized = serde_json::to_string(&db); assert!(serialized.is_ok()); @@ -582,16 +555,8 @@ mod tests { } #[wasm_bindgen_test] - fn test_sqlite_wasm_database_deserialization() { - let json_str = r#"{"dbName":"testdb"}"#; - let result: Result = serde_json::from_str(json_str); - - assert!(result.is_ok(), "Should be able to deserialize empty object"); - } - - #[wasm_bindgen_test] - fn test_sqlite_wasm_database_creation() { - let result = SQLiteWasmDatabase::new("testdb"); + async fn test_sqlite_wasm_database_creation() { + let result = SQLiteWasmDatabase::new("testdb").await; match result { Ok(_db) => {} @@ -604,7 +569,7 @@ mod tests { #[wasm_bindgen_test] async fn test_query_message_format() { - if let Ok(db) = SQLiteWasmDatabase::new("testdb") { + if let Ok(db) = SQLiteWasmDatabase::new("testdb").await { let result = db.query("SELECT 1", None).await; match result { @@ -700,7 +665,7 @@ mod tests { #[wasm_bindgen_test] async fn test_query_with_various_param_types_and_normalization() { install_post_message_spy(); - let db = SQLiteWasmDatabase::new("testdb").expect("db created"); + let db = SQLiteWasmDatabase::new("testdb").await.expect("db created"); // Build a params array with all requested JS value types let arr = js_sys::Array::new(); @@ -802,7 +767,7 @@ mod tests { #[wasm_bindgen_test] async fn test_query_params_presence_empty_array_vs_none() { install_post_message_spy(); - let db = SQLiteWasmDatabase::new("testdb").expect("db created"); + let db = SQLiteWasmDatabase::new("testdb").await.expect("db created"); // Case: None -> no params property on message let _ = db.query("SELECT 1", None).await; @@ -836,7 +801,7 @@ mod tests { #[wasm_bindgen_test] async fn test_query_rejects_nan_infinity_params() { - let db = SQLiteWasmDatabase::new("testdb").expect("db created"); + let db = SQLiteWasmDatabase::new("testdb").await.expect("db created"); // NaN { From 6c87953bd6e62c045f1febdb8c181d942b19909b Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 10 Nov 2025 13:27:09 +0100 Subject: [PATCH 04/15] run bundler --- pkg/package.json | 2 +- svelte-test/package-lock.json | 498 +++++++++++++++++----------------- svelte-test/package.json | 2 +- 3 files changed, 251 insertions(+), 251 deletions(-) diff --git a/pkg/package.json b/pkg/package.json index 9f053d8..45be3f9 100644 --- a/pkg/package.json +++ b/pkg/package.json @@ -12,4 +12,4 @@ "sideEffects": [ "./snippets/*" ] -} +} \ No newline at end of file diff --git a/svelte-test/package-lock.json b/svelte-test/package-lock.json index 7b1f650..44a7cf1 100644 --- a/svelte-test/package-lock.json +++ b/svelte-test/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "dependencies": { "@rainlanguage/float": "^0.0.0-alpha.22", - "@rainlanguage/sqlite-web": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.6.tgz" + "@rainlanguage/sqlite-web": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.7.tgz" }, "devDependencies": { "@sveltejs/adapter-auto": "^6.0.0", @@ -70,9 +70,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -87,9 +87,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -104,9 +104,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -121,9 +121,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -138,9 +138,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -155,9 +155,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -172,9 +172,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -189,9 +189,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -206,9 +206,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -223,9 +223,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -240,9 +240,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -257,9 +257,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -274,9 +274,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -291,9 +291,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -308,9 +308,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -325,9 +325,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -342,9 +342,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -359,9 +359,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -376,9 +376,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -393,9 +393,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -410,9 +410,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -427,9 +427,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -444,9 +444,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -461,9 +461,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -478,9 +478,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -495,9 +495,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -661,9 +661,9 @@ "license": "BSD-3-Clause" }, "node_modules/@inquirer/ansi": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", - "integrity": "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", "dev": true, "license": "MIT", "engines": { @@ -671,14 +671,14 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.19", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.19.tgz", - "integrity": "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==", + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.20.tgz", + "integrity": "sha512-HDGiWh2tyRZa0M1ZnEIUCQro25gW/mN8ODByicQrbR1yHx4hT+IOpozCMi5TgBtUdklLwRI2mv14eNpftDluEw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.0", - "@inquirer/type": "^3.0.9" + "@inquirer/core": "^10.3.1", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -693,20 +693,20 @@ } }, "node_modules/@inquirer/core": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.0.tgz", - "integrity": "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.1.tgz", + "integrity": "sha512-hzGKIkfomGFPgxKmnKEKeA+uCYBqC+TKtRx5LgyHRCrF6S2MliwRIjp3sUaWwVzMp7ZXVs8elB0Tfe682Rpg4w==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.1", - "@inquirer/figures": "^1.0.14", - "@inquirer/type": "^3.0.9", + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", + "mute-stream": "^3.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -721,9 +721,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.14.tgz", - "integrity": "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", "dev": true, "license": "MIT", "engines": { @@ -731,9 +731,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.9.tgz", - "integrity": "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", "dev": true, "license": "MIT", "engines": { @@ -912,14 +912,14 @@ } }, "node_modules/@rainlanguage/sqlite-web": { - "version": "0.0.1-alpha.6", - "resolved": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.6.tgz", - "integrity": "sha512-nZnLI9hFxa7hsTyNFzCl8aZngVsTfowgXNWx3SbReW1fh2HjqKpp6cfR+I/7PgbSaw4GTmXVJxzYFcj40fUSkg==" + "version": "0.0.1-alpha.7", + "resolved": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.7.tgz", + "integrity": "sha512-LFVuijhBVkJYzJjomTJmZA+cTt3WMZJvpO5E6lYk1rXf2YaqZoT7IoAz/x/fRaDeyLzdppUiqGfBfHrNwcFNQw==" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", - "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", "cpu": [ "arm" ], @@ -931,9 +931,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", - "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", "cpu": [ "arm64" ], @@ -945,9 +945,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", - "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", "cpu": [ "arm64" ], @@ -959,9 +959,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", - "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", "cpu": [ "x64" ], @@ -973,9 +973,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", - "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", "cpu": [ "arm64" ], @@ -987,9 +987,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", - "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", "cpu": [ "x64" ], @@ -1001,9 +1001,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", - "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", "cpu": [ "arm" ], @@ -1015,9 +1015,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", - "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", "cpu": [ "arm" ], @@ -1029,9 +1029,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", - "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", "cpu": [ "arm64" ], @@ -1043,9 +1043,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", - "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", "cpu": [ "arm64" ], @@ -1057,9 +1057,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", - "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", "cpu": [ "loong64" ], @@ -1071,9 +1071,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", - "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", "cpu": [ "ppc64" ], @@ -1085,9 +1085,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", - "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", "cpu": [ "riscv64" ], @@ -1099,9 +1099,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", - "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", "cpu": [ "riscv64" ], @@ -1113,9 +1113,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", - "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", "cpu": [ "s390x" ], @@ -1127,9 +1127,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", - "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", "cpu": [ "x64" ], @@ -1141,9 +1141,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", - "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", "cpu": [ "x64" ], @@ -1155,9 +1155,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", - "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", "cpu": [ "arm64" ], @@ -1169,9 +1169,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", - "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", "cpu": [ "arm64" ], @@ -1183,9 +1183,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", - "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", "cpu": [ "ia32" ], @@ -1197,9 +1197,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", - "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", "cpu": [ "x64" ], @@ -1211,9 +1211,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", - "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", "cpu": [ "x64" ], @@ -1252,9 +1252,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.48.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.1.tgz", - "integrity": "sha512-CuwgzfHyc8OGI0HNa7ISQHN8u8XyLGM4jeP8+PYig2B15DD9H39KvwQJiUbGU44VsLx3NfwH4OXavIjvp7/6Ww==", + "version": "2.48.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.4.tgz", + "integrity": "sha512-TGFX1pZUt9qqY20Cv5NyYvy0iLWHf2jXi8s+eCGsig7jQMdwZWKUFMR6TbvFNhfDSUpc1sH/Y5EHv20g3HHA3g==", "dev": true, "license": "MIT", "dependencies": { @@ -1403,9 +1403,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "dev": true, "license": "MIT", "dependencies": { @@ -2819,9 +2819,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", - "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2832,32 +2832,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -3128,9 +3128,9 @@ } }, "node_modules/esrap": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.1.tgz", - "integrity": "sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz", + "integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==", "dev": true, "license": "MIT", "dependencies": { @@ -3466,9 +3466,9 @@ "license": "MIT" }, "node_modules/graphql": { - "version": "16.11.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", - "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", + "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "dev": true, "license": "MIT", "engines": { @@ -3862,9 +3862,9 @@ "license": "MIT" }, "node_modules/msw": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.11.6.tgz", - "integrity": "sha512-MCYMykvmiYScyUm7I6y0VCxpNq1rgd5v7kG8ks5dKtvmxRUUPjribX6mUoUNBbM5/3PhUyoelEWiKXGOz84c+w==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.1.tgz", + "integrity": "sha512-arzsi9IZjjByiEw21gSUP82qHM8zkV69nNpWV6W4z72KiLvsDWoOp678ORV6cNfU/JGhlX0SsnD4oXo9gI6I2A==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3930,13 +3930,13 @@ } }, "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/nanoid": { @@ -4464,9 +4464,9 @@ } }, "node_modules/rollup": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", - "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", "dev": true, "license": "MIT", "dependencies": { @@ -4480,28 +4480,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" } }, @@ -4726,9 +4726,9 @@ } }, "node_modules/svelte": { - "version": "5.42.3", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.42.3.tgz", - "integrity": "sha512-+8dUmdJGvKSWEfbAgIaUmpD97s1bBAGxEf6s7wQonk+HNdMmrBZtpStzRypRqrYBFUmmhaUgBHUjraE8gLqWAw==", + "version": "5.43.5", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.5.tgz", + "integrity": "sha512-HQoZArIewxQVNedseDsgMgnRSC4XOXczxXLF9rOJaPIJkg58INOPUiL8aEtzqZIXNSZJyw8NmqObwg/voajiHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5327,9 +5327,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.1.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", - "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/svelte-test/package.json b/svelte-test/package.json index 633f3e7..9c4184b 100644 --- a/svelte-test/package.json +++ b/svelte-test/package.json @@ -41,6 +41,6 @@ "type": "module", "dependencies": { "@rainlanguage/float": "^0.0.0-alpha.22", - "@rainlanguage/sqlite-web": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.6.tgz" + "@rainlanguage/sqlite-web": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.7.tgz" } } From c87f0ee2445acc5e1eafbcb6907760adc7420b1c Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 10 Nov 2025 13:27:20 +0100 Subject: [PATCH 05/15] Update existing UI tests --- svelte-test/src/routes/+page.svelte | 6 +----- svelte-test/src/routes/sql/+page.svelte | 5 +---- svelte-test/tests/fixtures/test-helpers.ts | 9 ++------- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/svelte-test/src/routes/+page.svelte b/svelte-test/src/routes/+page.svelte index c4c73c9..26e98cd 100644 --- a/svelte-test/src/routes/+page.svelte +++ b/svelte-test/src/routes/+page.svelte @@ -20,16 +20,12 @@ await init(); status = 'Creating database connection...'; - let res = SQLiteWasmDatabase.new('ui-app-db'); + let res = await SQLiteWasmDatabase.new('ui-app-db'); if (res.error) { throw new Error('Failed to create database connection'); } db = res.value; - status = 'Waiting for worker to be ready...'; - // Wait for worker to be ready - await new Promise(resolve => setTimeout(resolve, 1000)); - status = 'Setting up database schema...'; // Initialize schema await db.query(` diff --git a/svelte-test/src/routes/sql/+page.svelte b/svelte-test/src/routes/sql/+page.svelte index 46a19d7..44aaa39 100644 --- a/svelte-test/src/routes/sql/+page.svelte +++ b/svelte-test/src/routes/sql/+page.svelte @@ -19,16 +19,13 @@ await init(); status = 'Creating database connection...'; - let res = SQLiteWasmDatabase.new('ui-sql-db'); + let res = await SQLiteWasmDatabase.new('ui-sql-db'); if (res.error) { status = `Failed to create database connection: ${res.error.msg}`; return; } db = res.value; - status = 'Waiting for worker to be ready...'; - await new Promise(resolve => setTimeout(resolve, 1000)); - status = 'Setting up database schema...'; await db.query(` CREATE TABLE IF NOT EXISTS users ( diff --git a/svelte-test/tests/fixtures/test-helpers.ts b/svelte-test/tests/fixtures/test-helpers.ts index e81462f..b3b2619 100644 --- a/svelte-test/tests/fixtures/test-helpers.ts +++ b/svelte-test/tests/fixtures/test-helpers.ts @@ -8,17 +8,12 @@ export async function createTestDatabase(name: string = 'ui-test-db'): Promise setTimeout(resolve, 1000)); - - return db; + return result.value!; } /** From d1b549eec5d652bd4c38db2e159d67292a747e54 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 10 Nov 2025 14:09:22 +0100 Subject: [PATCH 06/15] Fix race condition in the leader selection --- packages/sqlite-web-core/src/coordination.rs | 69 +++++++++++++++++++- packages/sqlite-web-core/src/messages.rs | 12 ++++ packages/sqlite-web-core/src/worker.rs | 11 +++- scripts/local-bundle.sh | 1 - svelte-test/package-lock.json | 2 +- 5 files changed, 89 insertions(+), 6 deletions(-) diff --git a/packages/sqlite-web-core/src/coordination.rs b/packages/sqlite-web-core/src/coordination.rs index a9815f9..00ed3fb 100644 --- a/packages/sqlite-web-core/src/coordination.rs +++ b/packages/sqlite-web-core/src/coordination.rs @@ -5,7 +5,7 @@ use std::rc::Rc; use uuid::Uuid; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -use wasm_bindgen_futures::spawn_local; +use wasm_bindgen_futures::{spawn_local, JsFuture}; use web_sys::{BroadcastChannel, DedicatedWorkerGlobalScope}; use crate::database::SQLiteDatabase; @@ -126,6 +126,7 @@ impl WorkerState { let db = Rc::clone(&self.db); let pending_queries = Rc::clone(&self.pending_queries); let channel = self.channel.clone(); + let worker_id = self.worker_id.clone(); let onmessage = Closure::wrap(Box::new(move |event: web_sys::MessageEvent| { let data = event.data(); @@ -136,6 +137,7 @@ impl WorkerState { &db, &channel, &pending_queries, + &worker_id, msg, ); } @@ -147,6 +149,33 @@ impl WorkerState { Ok(()) } + pub fn start_leader_probe(self: &Rc) { + if *self.is_leader.borrow() { + return; + } + let has_leader = Rc::clone(&self.has_leader); + let channel = self.channel.clone(); + let worker_id = self.worker_id.clone(); + spawn_local(async move { + const MAX_ATTEMPTS: u32 = 40; + let mut attempts = 0; + while attempts < MAX_ATTEMPTS { + attempts += 1; + if { *has_leader.borrow() } { + break; + } + let ping = ChannelMessage::LeaderPing { + requester_id: worker_id.clone(), + }; + if let Err(err_msg) = send_channel_message(&channel, &ping) { + let _ = send_worker_error_message(&err_msg); + break; + } + sleep_ms(250).await; + } + }); + } + pub async fn attempt_leadership(&self) -> Result<(), JsValue> { let worker_id = self.worker_id.clone(); let is_leader = Rc::clone(&self.is_leader); @@ -271,6 +300,7 @@ fn handle_channel_message( db: &Rc>>, channel: &BroadcastChannel, pending_queries: &Rc>>, + worker_id: &str, msg: ChannelMessage, ) { match msg { @@ -302,7 +332,26 @@ fn handle_channel_message( error, } => handle_query_response(pending_queries, query_id, result, error), ChannelMessage::NewLeader { leader_id: _ } => { - *has_leader.borrow_mut() = true; + let mut has_leader_ref = has_leader.borrow_mut(); + let already_had_leader = *has_leader_ref; + *has_leader_ref = true; + drop(has_leader_ref); + + if !already_had_leader { + if let Err(err_msg) = send_worker_ready_message() { + let _ = send_worker_error_message(&err_msg); + } + } + } + ChannelMessage::LeaderPing { requester_id: _ } => { + if *is_leader.borrow() { + let response = ChannelMessage::NewLeader { + leader_id: worker_id.to_string(), + }; + if let Err(err_msg) = send_channel_message(channel, &response) { + let _ = send_worker_error_message(&err_msg); + } + } } } } @@ -341,6 +390,22 @@ fn handle_query_response( } } +async fn sleep_ms(ms: i32) { + let promise = js_sys::Promise::new(&mut |resolve, _| { + let global = js_sys::global(); + let scope: DedicatedWorkerGlobalScope = global.unchecked_into(); + let closure = Closure::once(move || { + let _ = resolve.call0(&JsValue::NULL); + }); + let _ = scope.set_timeout_with_callback_and_timeout_and_arguments_0( + closure.as_ref().unchecked_ref(), + ms, + ); + closure.forget(); + }); + let _ = JsFuture::from(promise).await; +} + async fn exec_on_db( db: Rc>>, sql: String, diff --git a/packages/sqlite-web-core/src/messages.rs b/packages/sqlite-web-core/src/messages.rs index f72495d..8b1fd3d 100644 --- a/packages/sqlite-web-core/src/messages.rs +++ b/packages/sqlite-web-core/src/messages.rs @@ -26,6 +26,11 @@ pub enum ChannelMessage { result: Option, error: Option, }, + #[serde(rename = "leader-ping")] + LeaderPing { + #[serde(rename = "requesterId")] + requester_id: String, + }, } // Messages from main thread @@ -124,6 +129,13 @@ mod tests { assert!(json.contains("\"error\":\"SQL syntax error\"")); assert!(json.contains("\"result\":null")); }); + + let leader_ping = ChannelMessage::LeaderPing { + requester_id: "worker-123".to_string(), + }; + assert_serialization_roundtrip(leader_ping, "leader-ping", |json| { + assert!(json.contains("\"requesterId\":\"worker-123\"")); + }); } #[wasm_bindgen_test] diff --git a/packages/sqlite-web-core/src/worker.rs b/packages/sqlite-web-core/src/worker.rs index 8fbf042..cb56b8f 100644 --- a/packages/sqlite-web-core/src/worker.rs +++ b/packages/sqlite-web-core/src/worker.rs @@ -127,9 +127,15 @@ fn handle_incoming_value(data: JsValue) { pub fn main() -> Result<(), JsValue> { console_error_panic_hook::set_once(); - let state = Rc::new(WorkerState::new()?); + let state = Rc::new(WorkerState::new().map_err(|err| { + let _ = send_worker_error(err.clone()); + err + })?); - state.setup_channel_listener()?; + state.setup_channel_listener().map_err(|err| { + let _ = send_worker_error(err.clone()); + err + })?; let state_clone = Rc::clone(&state); spawn_local(async move { @@ -143,6 +149,7 @@ pub fn main() -> Result<(), JsValue> { WORKER_STATE.with(|s| { *s.borrow_mut() = Some(Rc::clone(&state)); }); + state.start_leader_probe(); // Setup message handler from main thread let global = js_sys::global(); diff --git a/scripts/local-bundle.sh b/scripts/local-bundle.sh index 6e070a1..774468e 100755 --- a/scripts/local-bundle.sh +++ b/scripts/local-bundle.sh @@ -72,7 +72,6 @@ JS_GLUE_PLACEHOLDER if (typeof wasm.worker_main === 'function') { wasm.worker_main(); console.log('[Worker] SQLite worker initialized successfully'); - self.postMessage({type: 'worker-ready'}); } else { throw new Error('worker_main function not found'); } diff --git a/svelte-test/package-lock.json b/svelte-test/package-lock.json index 44a7cf1..f56fdca 100644 --- a/svelte-test/package-lock.json +++ b/svelte-test/package-lock.json @@ -914,7 +914,7 @@ "node_modules/@rainlanguage/sqlite-web": { "version": "0.0.1-alpha.7", "resolved": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.7.tgz", - "integrity": "sha512-LFVuijhBVkJYzJjomTJmZA+cTt3WMZJvpO5E6lYk1rXf2YaqZoT7IoAz/x/fRaDeyLzdppUiqGfBfHrNwcFNQw==" + "integrity": "sha512-fXRCkYSWqU1qJtyl7NEnCz+QrgeqIpjTmDw+OqgWvOiZmdKvhb48J+hSMGqe1f6o5qtyG1ka7kW8Y5AcOexW0A==" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.53.2", From 950b695a3249ecc3fb7b64e6ac2599225eafaa74 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 10 Nov 2025 14:11:20 +0100 Subject: [PATCH 07/15] run formatter --- packages/sqlite-web-core/src/coordination.rs | 2 +- packages/sqlite-web-core/src/worker.rs | 6 ++---- svelte-test/package-lock.json | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/sqlite-web-core/src/coordination.rs b/packages/sqlite-web-core/src/coordination.rs index 00ed3fb..a6f5943 100644 --- a/packages/sqlite-web-core/src/coordination.rs +++ b/packages/sqlite-web-core/src/coordination.rs @@ -161,7 +161,7 @@ impl WorkerState { let mut attempts = 0; while attempts < MAX_ATTEMPTS { attempts += 1; - if { *has_leader.borrow() } { + if *has_leader.borrow() { break; } let ping = ChannelMessage::LeaderPing { diff --git a/packages/sqlite-web-core/src/worker.rs b/packages/sqlite-web-core/src/worker.rs index cb56b8f..cbb3c35 100644 --- a/packages/sqlite-web-core/src/worker.rs +++ b/packages/sqlite-web-core/src/worker.rs @@ -127,14 +127,12 @@ fn handle_incoming_value(data: JsValue) { pub fn main() -> Result<(), JsValue> { console_error_panic_hook::set_once(); - let state = Rc::new(WorkerState::new().map_err(|err| { + let state = Rc::new(WorkerState::new().inspect_err(|err| { let _ = send_worker_error(err.clone()); - err })?); - state.setup_channel_listener().map_err(|err| { + state.setup_channel_listener().inspect_err(|err| { let _ = send_worker_error(err.clone()); - err })?; let state_clone = Rc::clone(&state); diff --git a/svelte-test/package-lock.json b/svelte-test/package-lock.json index f56fdca..0023284 100644 --- a/svelte-test/package-lock.json +++ b/svelte-test/package-lock.json @@ -914,7 +914,7 @@ "node_modules/@rainlanguage/sqlite-web": { "version": "0.0.1-alpha.7", "resolved": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.7.tgz", - "integrity": "sha512-fXRCkYSWqU1qJtyl7NEnCz+QrgeqIpjTmDw+OqgWvOiZmdKvhb48J+hSMGqe1f6o5qtyG1ka7kW8Y5AcOexW0A==" + "integrity": "sha512-k01wW2i/MMLwPklXl1OQRZxLrY8R0W6G7CFFStJQ0Er4qhgdryJIzxg4gZjUZSkNqv7CYzaO8uGqXjWLDHMyjw==" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.53.2", From e5969d6dd2c9b19abf6e95060a65d13f5a93c46e Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 10 Nov 2025 14:29:41 +0100 Subject: [PATCH 08/15] Refactor --- packages/sqlite-web-core/src/coordination.rs | 40 +++- packages/sqlite-web/src/lib.rs | 217 ++++++++++--------- svelte-test/package-lock.json | 2 +- 3 files changed, 144 insertions(+), 115 deletions(-) diff --git a/packages/sqlite-web-core/src/coordination.rs b/packages/sqlite-web-core/src/coordination.rs index a6f5943..6eb2009 100644 --- a/packages/sqlite-web-core/src/coordination.rs +++ b/packages/sqlite-web-core/src/coordination.rs @@ -288,7 +288,7 @@ impl WorkerState { Ok(val) => val .as_string() .ok_or_else(|| "Invalid response".to_string()), - Err(e) => Err(format!("{e:?}")), + Err(e) => Err(js_value_to_string(&e)), } } } @@ -392,15 +392,39 @@ fn handle_query_response( async fn sleep_ms(ms: i32) { let promise = js_sys::Promise::new(&mut |resolve, _| { - let global = js_sys::global(); - let scope: DedicatedWorkerGlobalScope = global.unchecked_into(); + let resolve_for_timeout = resolve.clone(); let closure = Closure::once(move || { - let _ = resolve.call0(&JsValue::NULL); + let _ = resolve_for_timeout.call0(&JsValue::NULL); }); - let _ = scope.set_timeout_with_callback_and_timeout_and_arguments_0( - closure.as_ref().unchecked_ref(), - ms, - ); + + let timeout_result = js_sys::global() + .dyn_into::() + .map_err(|_| ()) + .and_then(|scope| { + scope + .set_timeout_with_callback_and_timeout_and_arguments_0( + closure.as_ref().unchecked_ref(), + ms, + ) + .map(|_| ()) + .map_err(|_| ()) + }) + .or_else(|_| { + web_sys::window().ok_or(()).and_then(|win| { + win.set_timeout_with_callback_and_timeout_and_arguments_0( + closure.as_ref().unchecked_ref(), + ms, + ) + .map(|_| ()) + .map_err(|_| ()) + }) + }); + + if timeout_result.is_err() { + // As a best-effort fallback, resolve immediately. + let _ = resolve.call0(&JsValue::NULL); + } + closure.forget(); }); let _ = JsFuture::from(promise).await; diff --git a/packages/sqlite-web/src/lib.rs b/packages/sqlite-web/src/lib.rs index 24106d2..f1d9f2a 100644 --- a/packages/sqlite-web/src/lib.rs +++ b/packages/sqlite-web/src/lib.rs @@ -1,6 +1,6 @@ use base64::Engine; use js_sys::Array; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; @@ -11,6 +11,7 @@ use wasm_bindgen_utils::prelude::*; use web_sys::{Blob, BlobPropertyBag, MessageEvent, Url, Worker}; mod worker_template; +use serde_wasm_bindgen::from_value; use worker_template::generate_self_contained_worker; fn create_worker_from_code(worker_code: &str) -> Result { @@ -30,25 +31,13 @@ fn create_worker_from_code(worker_code: &str) -> Result { fn install_onmessage_handler( worker: &Worker, pending_queries: Rc>>, - ready_state: Rc>, - ready_resolve: Rc>>, - ready_reject: Rc>>, - ready_promise: Rc>>, + ready_signal: ReadySignal, ) { let pending_queries_clone = Rc::clone(&pending_queries); - let ready_state_clone = Rc::clone(&ready_state); - let ready_resolve_clone = Rc::clone(&ready_resolve); - let ready_reject_clone = Rc::clone(&ready_reject); - let ready_promise_clone = Rc::clone(&ready_promise); + let ready_signal_clone = ready_signal.clone(); let onmessage = Closure::wrap(Box::new(move |event: MessageEvent| { let data = event.data(); - if handle_worker_control_message( - &data, - &ready_state_clone, - &ready_resolve_clone, - &ready_reject_clone, - &ready_promise_clone, - ) { + if handle_worker_control_message(&data, &ready_signal_clone) { return; } handle_query_result_message(&data, &pending_queries_clone); @@ -58,51 +47,32 @@ fn install_onmessage_handler( onmessage.forget(); } -fn handle_worker_control_message( - data: &JsValue, - ready_state: &Rc>, - ready_resolve: &Rc>>, - ready_reject: &Rc>>, - ready_promise: &Rc>>, -) -> bool { - if let Ok(obj) = js_sys::Reflect::get(data, &JsValue::from_str("type")) { - if let Some(msg_type) = obj.as_string() { - if msg_type == "worker-ready" { - { - let mut state = ready_state.borrow_mut(); - if matches!(*state, InitializationState::Ready) { - return true; - } - *state = InitializationState::Ready; - } - if let Some(resolve) = ready_resolve.borrow_mut().take() { - let _ = resolve.call0(&JsValue::NULL); - } - ready_reject.borrow_mut().take(); - ready_promise.borrow_mut().take(); - return true; - } else if msg_type == "worker-error" { - let error_value = js_sys::Reflect::get(data, &JsValue::from_str("error")) - .ok() - .unwrap_or(JsValue::from_str("Unknown worker error")); - let error_text = describe_js_value(&error_value); - { - let mut state = ready_state.borrow_mut(); - if !matches!(&*state, InitializationState::Failed(existing) if existing == &error_text) - { - *state = InitializationState::Failed(error_text.clone()); - } - } - ready_resolve.borrow_mut().take(); - if let Some(reject) = ready_reject.borrow_mut().take() { - let _ = reject.call1(&JsValue::NULL, &JsValue::from_str(&error_text)); - } - ready_promise.borrow_mut().take(); - return true; - } +fn handle_worker_control_message(data: &JsValue, ready_signal: &ReadySignal) -> bool { + match from_value::(data.clone()) { + Ok(WorkerControlMessage::Ready) => { + ready_signal.mark_ready(); + true + } + Ok(WorkerControlMessage::Error) => { + let reason = js_sys::Reflect::get(data, &JsValue::from_str("error")) + .ok() + .filter(|val| !val.is_null() && !val.is_undefined()) + .map(|val| describe_js_value(&val)) + .unwrap_or_else(|| "Unknown worker error".to_string()); + ready_signal.mark_failed(reason); + true } + Err(_) => false, } - false +} + +#[derive(Deserialize)] +#[serde(tag = "type")] +enum WorkerControlMessage { + #[serde(rename = "worker-ready")] + Ready, + #[serde(rename = "worker-error")] + Error, } fn handle_query_result_message( @@ -219,15 +189,80 @@ enum InitializationState { Failed(String), } +#[derive(Clone)] +struct ReadySignal { + state: Rc>, + resolve: Rc>>, + reject: Rc>>, + promise: Rc>>, +} + +impl ReadySignal { + fn new() -> Self { + let state = Rc::new(RefCell::new(InitializationState::Pending)); + let resolve = Rc::new(RefCell::new(None)); + let reject = Rc::new(RefCell::new(None)); + let promise = Rc::new(RefCell::new(None)); + { + let ready_promise = create_ready_promise(&resolve, &reject); + promise.borrow_mut().replace(ready_promise); + } + Self { + state, + resolve, + reject, + promise, + } + } + + fn current_state(&self) -> InitializationState { + self.state.borrow().clone() + } + + fn wait_promise(&self) -> Result { + self.promise.borrow().as_ref().cloned().ok_or_else(|| { + SQLiteWasmDatabaseError::InitializationFailed( + "Worker readiness promise missing".to_string(), + ) + }) + } + + fn mark_ready(&self) { + { + let mut state = self.state.borrow_mut(); + if matches!(*state, InitializationState::Ready) { + return; + } + *state = InitializationState::Ready; + } + if let Some(resolve) = self.resolve.borrow_mut().take() { + let _ = resolve.call0(&JsValue::NULL); + } + self.reject.borrow_mut().take(); + self.promise.borrow_mut().take(); + } + + fn mark_failed(&self, reason: String) { + { + let mut state = self.state.borrow_mut(); + if !matches!(&*state, InitializationState::Failed(existing) if existing == &reason) { + *state = InitializationState::Failed(reason.clone()); + } + } + self.resolve.borrow_mut().take(); + if let Some(reject) = self.reject.borrow_mut().take() { + let _ = reject.call1(&JsValue::NULL, &JsValue::from_str(&reason)); + } + self.promise.borrow_mut().take(); + } +} + #[wasm_bindgen] pub struct SQLiteWasmDatabase { worker: Worker, pending_queries: Rc>>, next_request_id: Rc>, - ready_state: Rc>, - ready_promise: Rc>>, - _ready_resolve: Rc>>, - _ready_reject: Rc>>, + ready_signal: ReadySignal, } impl Serialize for SQLiteWasmDatabase { @@ -331,32 +366,15 @@ impl SQLiteWasmDatabase { let pending_queries: Rc>> = Rc::new(RefCell::new(HashMap::new())); - let ready_state = Rc::new(RefCell::new(InitializationState::Pending)); - let ready_resolve_cell = Rc::new(RefCell::new(None)); - let ready_reject_cell = Rc::new(RefCell::new(None)); - let ready_promise = Rc::new(RefCell::new(None)); - { - let promise = create_ready_promise(&ready_resolve_cell, &ready_reject_cell); - ready_promise.borrow_mut().replace(promise); - } - install_onmessage_handler( - &worker, - Rc::clone(&pending_queries), - Rc::clone(&ready_state), - Rc::clone(&ready_resolve_cell), - Rc::clone(&ready_reject_cell), - Rc::clone(&ready_promise), - ); + let ready_signal = ReadySignal::new(); + install_onmessage_handler(&worker, Rc::clone(&pending_queries), ready_signal.clone()); let next_request_id = Rc::new(RefCell::new(1u32)); Ok(SQLiteWasmDatabase { worker, pending_queries, next_request_id, - ready_state, - ready_promise, - _ready_resolve: ready_resolve_cell, - _ready_reject: ready_reject_cell, + ready_signal, }) } @@ -370,40 +388,29 @@ impl SQLiteWasmDatabase { } async fn wait_until_ready(&self) -> Result<(), SQLiteWasmDatabaseError> { - match &*self.ready_state.borrow() { + match self.ready_signal.current_state() { InitializationState::Ready => return Ok(()), InitializationState::Failed(reason) => { - return Err(SQLiteWasmDatabaseError::InitializationFailed( - reason.clone(), - )); + return Err(SQLiteWasmDatabaseError::InitializationFailed(reason)); } InitializationState::Pending => {} } - let promise = self - .ready_promise - .borrow() - .as_ref() - .cloned() - .ok_or_else(|| { - SQLiteWasmDatabaseError::InitializationFailed( - "Worker readiness promise missing".to_string(), - ) - })?; + let promise = self.ready_signal.wait_promise()?; match JsFuture::from(promise).await { - Ok(_) => match &*self.ready_state.borrow() { + Ok(_) => match self.ready_signal.current_state() { InitializationState::Ready => Ok(()), - InitializationState::Failed(reason) => Err( - SQLiteWasmDatabaseError::InitializationFailed(reason.clone()), - ), + InitializationState::Failed(reason) => { + Err(SQLiteWasmDatabaseError::InitializationFailed(reason)) + } InitializationState::Pending => Err(SQLiteWasmDatabaseError::InitializationFailed( "Worker failed to signal readiness".to_string(), )), }, Err(err) => { let reason = describe_js_value(&err); - *self.ready_state.borrow_mut() = InitializationState::Failed(reason.clone()); + self.ready_signal.mark_failed(reason.clone()); Err(SQLiteWasmDatabaseError::InitializationFailed(reason)) } } @@ -424,10 +431,8 @@ impl SQLiteWasmDatabase { let params_js = params.map(JsValue::from).unwrap_or(JsValue::UNDEFINED); let params_array = Self::normalize_params_js(¶ms_js)?; - if let InitializationState::Failed(reason) = &*self.ready_state.borrow() { - return Err(SQLiteWasmDatabaseError::InitializationFailed( - reason.clone(), - )); + if let InitializationState::Failed(reason) = self.ready_signal.current_state() { + return Err(SQLiteWasmDatabaseError::InitializationFailed(reason)); } // Build the message object up-front and propagate errors diff --git a/svelte-test/package-lock.json b/svelte-test/package-lock.json index 0023284..2130be8 100644 --- a/svelte-test/package-lock.json +++ b/svelte-test/package-lock.json @@ -914,7 +914,7 @@ "node_modules/@rainlanguage/sqlite-web": { "version": "0.0.1-alpha.7", "resolved": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.7.tgz", - "integrity": "sha512-k01wW2i/MMLwPklXl1OQRZxLrY8R0W6G7CFFStJQ0Er4qhgdryJIzxg4gZjUZSkNqv7CYzaO8uGqXjWLDHMyjw==" + "integrity": "sha512-d+tuD5keEVZu2U8qGU1AHGTiVDLKVA8O4ei6JxPciv5WiZEFZ605KH6rSClUogkPOOBl7fcnhlKouiOojBKnYg==" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.53.2", From af59be7d856798304e61ecc42aead713868174a8 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 10 Nov 2025 14:34:30 +0100 Subject: [PATCH 09/15] Update tests --- packages/sqlite-web-core/src/coordination.rs | 5 ++ packages/sqlite-web/src/lib.rs | 65 ++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/packages/sqlite-web-core/src/coordination.rs b/packages/sqlite-web-core/src/coordination.rs index 6eb2009..2f20204 100644 --- a/packages/sqlite-web-core/src/coordination.rs +++ b/packages/sqlite-web-core/src/coordination.rs @@ -733,4 +733,9 @@ mod tests { ); } } + + #[wasm_bindgen_test(async)] + async fn test_sleep_ms_completes() { + sleep_ms(0).await; + } } diff --git a/packages/sqlite-web/src/lib.rs b/packages/sqlite-web/src/lib.rs index f1d9f2a..0e67f00 100644 --- a/packages/sqlite-web/src/lib.rs +++ b/packages/sqlite-web/src/lib.rs @@ -509,6 +509,20 @@ mod tests { wasm_bindgen_test_configure!(run_in_browser); + fn make_control_message(msg_type: &str, error: Option<&str>) -> JsValue { + let msg = js_sys::Object::new(); + let _ = js_sys::Reflect::set( + &msg, + &JsValue::from_str("type"), + &JsValue::from_str(msg_type), + ); + if let Some(err) = error { + let _ = + js_sys::Reflect::set(&msg, &JsValue::from_str("error"), &JsValue::from_str(err)); + } + msg.into() + } + #[wasm_bindgen_test] fn test_sqlite_wasm_database_error_from_js_value() { let js_error = JsValue::from_str("Test error message"); @@ -617,6 +631,57 @@ mod tests { ); } + #[wasm_bindgen_test(async)] + async fn test_ready_signal_resolves_on_worker_ready_msg() { + let signal = ReadySignal::new(); + let promise = signal + .wait_promise() + .expect("ready promise should exist before resolution"); + let future = JsFuture::from(promise); + + let message = make_control_message("worker-ready", None); + let handled = handle_worker_control_message(&message, &signal); + + assert!(handled, "Ready message should be handled"); + assert!( + matches!(signal.current_state(), InitializationState::Ready), + "Signal should transition to Ready state" + ); + let result = future.await; + assert!( + result.is_ok(), + "Ready promise should resolve successfully, got {result:?}" + ); + } + + #[wasm_bindgen_test(async)] + async fn test_ready_signal_rejects_on_worker_error_msg() { + let signal = ReadySignal::new(); + let promise = signal + .wait_promise() + .expect("ready promise should exist before resolution"); + let future = JsFuture::from(promise); + + let message = make_control_message("worker-error", Some("boom")); + let handled = handle_worker_control_message(&message, &signal); + + assert!(handled, "Error message should be handled"); + match signal.current_state() { + InitializationState::Failed(reason) => { + assert_eq!(reason, "boom", "Failure reason should match payload") + } + other => panic!("Expected Failed state, got {other:?}"), + } + let err = future + .await + .expect_err("Promise should reject on worker-error"); + assert_eq!( + err.as_string().as_deref(), + Some("boom"), + "Rejected value should propagate worker error text" + ); + } + // --- Test helpers for spying on Worker.prototype.postMessage --- fn install_post_message_spy() { // Wrap Worker.prototype.postMessage to capture the message argument From 8db2c3a3992dc754b0c6722f8e1344a505026cdb Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 10 Nov 2025 14:48:04 +0100 Subject: [PATCH 10/15] split functionality --- packages/sqlite-web/src/db.rs | 185 ++++++ packages/sqlite-web/src/errors.rs | 36 ++ packages/sqlite-web/src/lib.rs | 905 +----------------------------- packages/sqlite-web/src/params.rs | 92 +++ packages/sqlite-web/src/ready.rs | 94 ++++ packages/sqlite-web/src/tests.rs | 352 ++++++++++++ packages/sqlite-web/src/utils.rs | 14 + packages/sqlite-web/src/worker.rs | 114 ++++ svelte-test/package-lock.json | 2 +- 9 files changed, 897 insertions(+), 897 deletions(-) create mode 100644 packages/sqlite-web/src/db.rs create mode 100644 packages/sqlite-web/src/errors.rs create mode 100644 packages/sqlite-web/src/params.rs create mode 100644 packages/sqlite-web/src/ready.rs create mode 100644 packages/sqlite-web/src/tests.rs create mode 100644 packages/sqlite-web/src/utils.rs create mode 100644 packages/sqlite-web/src/worker.rs diff --git a/packages/sqlite-web/src/db.rs b/packages/sqlite-web/src/db.rs new file mode 100644 index 0000000..b013541 --- /dev/null +++ b/packages/sqlite-web/src/db.rs @@ -0,0 +1,185 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + +use js_sys::Array; +use serde::Serialize; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; +use wasm_bindgen_utils::prelude::*; +use web_sys::Worker; + +use crate::errors::SQLiteWasmDatabaseError; +use crate::params::normalize_params_js; +use crate::ready::{InitializationState, ReadySignal}; +use crate::utils::describe_js_value; +use crate::worker::{create_worker_from_code, install_onmessage_handler}; +use crate::worker_template::generate_self_contained_worker; + +#[wasm_bindgen] +pub struct SQLiteWasmDatabase { + worker: Worker, + pending_queries: Rc>>, + next_request_id: Rc>, + ready_signal: ReadySignal, +} + +impl Serialize for SQLiteWasmDatabase { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let state = serializer.serialize_struct("SQLiteWasmDatabase", 0)?; + state.end() + } +} + +#[wasm_export] +impl SQLiteWasmDatabase { + /// Create a new database connection with fully embedded worker + #[wasm_export(js_name = "new", preserve_js_class)] + pub async fn new(db_name: &str) -> Result { + let db_name = db_name.trim(); + if db_name.is_empty() { + return Err(SQLiteWasmDatabaseError::JsError(JsValue::from_str( + "Database name is required", + ))); + } + let db = Self::construct(db_name)?; + db.wait_until_ready().await?; + Ok(db) + } + + fn construct(db_name: &str) -> Result { + let worker_code = generate_self_contained_worker(db_name); + let worker = create_worker_from_code(&worker_code)?; + + let pending_queries: Rc>> = + Rc::new(RefCell::new(HashMap::new())); + let ready_signal = ReadySignal::new(); + install_onmessage_handler(&worker, Rc::clone(&pending_queries), ready_signal.clone()); + let next_request_id = Rc::new(RefCell::new(1u32)); + + Ok(SQLiteWasmDatabase { + worker, + pending_queries, + next_request_id, + ready_signal, + }) + } + + fn normalize_params(params: Option) -> Result { + let params_js = params.map(JsValue::from).unwrap_or(JsValue::UNDEFINED); + normalize_params_js(¶ms_js) + } + + async fn wait_until_ready(&self) -> Result<(), SQLiteWasmDatabaseError> { + match self.ready_signal.current_state() { + InitializationState::Ready => return Ok(()), + InitializationState::Failed(reason) => { + return Err(SQLiteWasmDatabaseError::InitializationFailed(reason)); + } + InitializationState::Pending => {} + } + + let promise = self.ready_signal.wait_promise()?; + + match JsFuture::from(promise).await { + Ok(_) => match self.ready_signal.current_state() { + InitializationState::Ready => Ok(()), + InitializationState::Failed(reason) => { + Err(SQLiteWasmDatabaseError::InitializationFailed(reason)) + } + InitializationState::Pending => Err(SQLiteWasmDatabaseError::InitializationFailed( + "Worker failed to signal readiness".to_string(), + )), + }, + Err(err) => { + let reason = describe_js_value(&err); + self.ready_signal.mark_failed(reason.clone()); + Err(SQLiteWasmDatabaseError::InitializationFailed(reason)) + } + } + } + + /// Execute a SQL query (optionally parameterized via JS Array) + /// + /// Passing `undefined`/`null` from JS maps to `None`. + #[wasm_export(js_name = "query", unchecked_return_type = "string")] + pub async fn query( + &self, + sql: &str, + params: Option, + ) -> Result { + let worker = &self.worker; + let pending_queries = Rc::clone(&self.pending_queries); + let sql = sql.to_string(); + let params_array = Self::normalize_params(params)?; + + if let InitializationState::Failed(reason) = self.ready_signal.current_state() { + return Err(SQLiteWasmDatabaseError::InitializationFailed(reason)); + } + + let message = js_sys::Object::new(); + js_sys::Reflect::set( + &message, + &JsValue::from_str("type"), + &JsValue::from_str("execute-query"), + ) + .map_err(SQLiteWasmDatabaseError::JsError)?; + + let request_id = { + let mut n = self.next_request_id.borrow_mut(); + let id = *n; + *n = n.wrapping_add(1).max(1); + id + }; + js_sys::Reflect::set( + &message, + &JsValue::from_str("requestId"), + &JsValue::from_f64(request_id as f64), + ) + .map_err(SQLiteWasmDatabaseError::JsError)?; + js_sys::Reflect::set( + &message, + &JsValue::from_str("sql"), + &JsValue::from_str(&sql), + ) + .map_err(SQLiteWasmDatabaseError::JsError)?; + if params_array.length() > 0 { + let params_js = JsValue::from(params_array.clone()); + js_sys::Reflect::set(&message, &JsValue::from_str("params"), ¶ms_js) + .map_err(SQLiteWasmDatabaseError::JsError)?; + } + + 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 result = match JsFuture::from(promise).await { + Ok(value) => value, + Err(err) => { + if err + .as_string() + .as_deref() + .map(|s| s == "InitializationPending") + .unwrap_or(false) + { + return Err(SQLiteWasmDatabaseError::InitializationPending); + } + return Err(SQLiteWasmDatabaseError::JsError(err)); + } + }; + Ok(result.as_string().unwrap_or_else(|| format!("{result:?}"))) + } +} diff --git a/packages/sqlite-web/src/errors.rs b/packages/sqlite-web/src/errors.rs new file mode 100644 index 0000000..c1c94ff --- /dev/null +++ b/packages/sqlite-web/src/errors.rs @@ -0,0 +1,36 @@ +use thiserror::Error; +use wasm_bindgen::prelude::*; +use wasm_bindgen_utils::prelude::{serde_wasm_bindgen, WasmEncodedError}; + +#[derive(Debug, Error)] +pub enum SQLiteWasmDatabaseError { + #[error(transparent)] + SerdeError(#[from] serde_wasm_bindgen::Error), + #[error("JavaScript error: {0:?}")] + JsError(JsValue), + #[error("Initialization pending")] + InitializationPending, + #[error("Initialization failed: {0}")] + InitializationFailed(String), +} + +impl From for SQLiteWasmDatabaseError { + fn from(value: JsValue) -> Self { + SQLiteWasmDatabaseError::JsError(value) + } +} + +impl From for JsValue { + fn from(value: SQLiteWasmDatabaseError) -> Self { + JsError::new(&value.to_string()).into() + } +} + +impl From for WasmEncodedError { + fn from(value: SQLiteWasmDatabaseError) -> Self { + WasmEncodedError { + msg: value.to_string(), + readable_msg: value.to_string(), + } + } +} diff --git a/packages/sqlite-web/src/lib.rs b/packages/sqlite-web/src/lib.rs index 0e67f00..35bd109 100644 --- a/packages/sqlite-web/src/lib.rs +++ b/packages/sqlite-web/src/lib.rs @@ -1,900 +1,13 @@ -use base64::Engine; -use js_sys::Array; -use serde::{Deserialize, Serialize}; -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; -use thiserror::Error; -use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::JsFuture; -use wasm_bindgen_utils::prelude::*; -use web_sys::{Blob, BlobPropertyBag, MessageEvent, Url, Worker}; - +mod db; +mod errors; +mod params; +mod ready; +mod utils; +mod worker; mod worker_template; -use serde_wasm_bindgen::from_value; -use worker_template::generate_self_contained_worker; - -fn create_worker_from_code(worker_code: &str) -> Result { - let blob_parts = Array::new(); - blob_parts.push(&JsValue::from_str(worker_code)); - - let blob_options = BlobPropertyBag::new(); - blob_options.set_type("application/javascript"); - - let blob = Blob::new_with_str_sequence_and_options(&blob_parts, &blob_options)?; - let worker_url = Url::create_object_url_with_blob(&blob)?; - let worker_res = Worker::new(&worker_url); - Url::revoke_object_url(&worker_url)?; - worker_res -} - -fn install_onmessage_handler( - worker: &Worker, - pending_queries: Rc>>, - ready_signal: ReadySignal, -) { - let pending_queries_clone = Rc::clone(&pending_queries); - let ready_signal_clone = ready_signal.clone(); - let onmessage = Closure::wrap(Box::new(move |event: MessageEvent| { - let data = event.data(); - if handle_worker_control_message(&data, &ready_signal_clone) { - return; - } - handle_query_result_message(&data, &pending_queries_clone); - }) as Box); - - worker.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); - onmessage.forget(); -} - -fn handle_worker_control_message(data: &JsValue, ready_signal: &ReadySignal) -> bool { - match from_value::(data.clone()) { - Ok(WorkerControlMessage::Ready) => { - ready_signal.mark_ready(); - true - } - Ok(WorkerControlMessage::Error) => { - let reason = js_sys::Reflect::get(data, &JsValue::from_str("error")) - .ok() - .filter(|val| !val.is_null() && !val.is_undefined()) - .map(|val| describe_js_value(&val)) - .unwrap_or_else(|| "Unknown worker error".to_string()); - ready_signal.mark_failed(reason); - true - } - Err(_) => false, - } -} - -#[derive(Deserialize)] -#[serde(tag = "type")] -enum WorkerControlMessage { - #[serde(rename = "worker-ready")] - Ready, - #[serde(rename = "worker-error")] - Error, -} - -fn handle_query_result_message( - data: &JsValue, - pending_queries: &Rc>>, -) { - let msg_type = js_sys::Reflect::get(data, &JsValue::from_str("type")) - .ok() - .and_then(|obj| obj.as_string()); - - let Some(msg_type) = msg_type else { return }; - if msg_type != "query-result" { - return; - } - - // Lookup by requestId - let req_id_js = js_sys::Reflect::get(data, &JsValue::from_str("requestId")).ok(); - let req_id = req_id_js.and_then(|v| v.as_f64()).map(|n| n as u32); - let Some(request_id) = req_id else { return }; - let entry = pending_queries.borrow_mut().remove(&request_id); - let Some((resolve, reject)) = entry else { - return; - }; - - let error = js_sys::Reflect::get(data, &JsValue::from_str("error")) - .ok() - .filter(|e| !e.is_null() && !e.is_undefined()); - - if let Some(error) = error { - let error_str = error.as_string().unwrap_or_else(|| format!("{error:?}")); - let _ = reject.call1(&JsValue::NULL, &JsValue::from_str(&error_str)); - return; - } - - if let Some(result) = js_sys::Reflect::get(data, &JsValue::from_str("result")) - .ok() - .filter(|r| !r.is_null() && !r.is_undefined()) - { - let result_str = result.as_string().unwrap_or_else(|| format!("{result:?}")); - let _ = resolve.call1(&JsValue::NULL, &JsValue::from_str(&result_str)); - } -} - -fn encode_bigint_to_obj(bi: js_sys::BigInt) -> Result { - let obj = js_sys::Object::new(); - let s = bi - .to_string(10) - .map_err(|e| SQLiteWasmDatabaseError::JsError(e.into()))?; - js_sys::Reflect::set( - &obj, - &JsValue::from_str("__type"), - &JsValue::from_str("bigint"), - ) - .map_err(SQLiteWasmDatabaseError::from)?; - js_sys::Reflect::set(&obj, &JsValue::from_str("value"), &JsValue::from(s)) - .map_err(SQLiteWasmDatabaseError::from)?; - Ok(obj.into()) -} - -fn encode_binary_to_obj(bytes: Vec) -> Result { - let b64 = base64::engine::general_purpose::STANDARD.encode(bytes); - let obj = js_sys::Object::new(); - js_sys::Reflect::set( - &obj, - &JsValue::from_str("__type"), - &JsValue::from_str("blob"), - ) - .map_err(SQLiteWasmDatabaseError::from)?; - js_sys::Reflect::set(&obj, &JsValue::from_str("base64"), &JsValue::from_str(&b64)) - .map_err(SQLiteWasmDatabaseError::from)?; - Ok(obj.into()) -} - -fn normalize_one_param(v: &JsValue, index: u32) -> Result { - if v.is_null() || v.is_undefined() { - return Ok(JsValue::NULL); - } - if let Ok(bi) = v.clone().dyn_into::() { - return encode_bigint_to_obj(bi); - } - if let Ok(typed) = v.clone().dyn_into::() { - return encode_binary_to_obj(typed.to_vec()); - } - if let Ok(buf) = v.clone().dyn_into::() { - let typed = js_sys::Uint8Array::new(&buf); - return encode_binary_to_obj(typed.to_vec()); - } - if let Some(n) = v.as_f64() { - if !n.is_finite() { - return Err(SQLiteWasmDatabaseError::JsError(JsValue::from_str( - &format!( - "Invalid numeric value at position {} (NaN/Infinity not supported.)", - index + 1 - ), - ))); - } - return Ok(JsValue::from_f64(n)); - } - if let Some(b) = v.as_bool() { - return Ok(JsValue::from_bool(b)); - } - if let Some(s) = v.as_string() { - return Ok(JsValue::from_str(&s)); - } - Err(SQLiteWasmDatabaseError::JsError(JsValue::from_str( - &format!("Unsupported parameter type at position {}", index + 1), - ))) -} - -#[derive(Debug, Clone)] -enum InitializationState { - Pending, - Ready, - Failed(String), -} - -#[derive(Clone)] -struct ReadySignal { - state: Rc>, - resolve: Rc>>, - reject: Rc>>, - promise: Rc>>, -} - -impl ReadySignal { - fn new() -> Self { - let state = Rc::new(RefCell::new(InitializationState::Pending)); - let resolve = Rc::new(RefCell::new(None)); - let reject = Rc::new(RefCell::new(None)); - let promise = Rc::new(RefCell::new(None)); - { - let ready_promise = create_ready_promise(&resolve, &reject); - promise.borrow_mut().replace(ready_promise); - } - Self { - state, - resolve, - reject, - promise, - } - } - - fn current_state(&self) -> InitializationState { - self.state.borrow().clone() - } - - fn wait_promise(&self) -> Result { - self.promise.borrow().as_ref().cloned().ok_or_else(|| { - SQLiteWasmDatabaseError::InitializationFailed( - "Worker readiness promise missing".to_string(), - ) - }) - } - - fn mark_ready(&self) { - { - let mut state = self.state.borrow_mut(); - if matches!(*state, InitializationState::Ready) { - return; - } - *state = InitializationState::Ready; - } - if let Some(resolve) = self.resolve.borrow_mut().take() { - let _ = resolve.call0(&JsValue::NULL); - } - self.reject.borrow_mut().take(); - self.promise.borrow_mut().take(); - } - fn mark_failed(&self, reason: String) { - { - let mut state = self.state.borrow_mut(); - if !matches!(&*state, InitializationState::Failed(existing) if existing == &reason) { - *state = InitializationState::Failed(reason.clone()); - } - } - self.resolve.borrow_mut().take(); - if let Some(reject) = self.reject.borrow_mut().take() { - let _ = reject.call1(&JsValue::NULL, &JsValue::from_str(&reason)); - } - self.promise.borrow_mut().take(); - } -} - -#[wasm_bindgen] -pub struct SQLiteWasmDatabase { - worker: Worker, - pending_queries: Rc>>, - next_request_id: Rc>, - ready_signal: ReadySignal, -} - -impl Serialize for SQLiteWasmDatabase { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let state = serializer.serialize_struct("SQLiteWasmDatabase", 0)?; - state.end() - } -} - -fn describe_js_value(value: &JsValue) -> String { - if let Some(s) = value.as_string() { - return s; - } - if let Some(n) = value.as_f64() { - if n.fract() == 0.0 { - return format!("{n:.0}"); - } - return format!("{n}"); - } - format!("{value:?}") -} - -fn create_ready_promise( - resolve_cell: &Rc>>, - reject_cell: &Rc>>, -) -> js_sys::Promise { - let resolve_clone = Rc::clone(resolve_cell); - let reject_clone = Rc::clone(reject_cell); - js_sys::Promise::new(&mut move |resolve, reject| { - resolve_clone.borrow_mut().replace(resolve); - reject_clone.borrow_mut().replace(reject); - }) -} -#[derive(Debug, Error)] -pub enum SQLiteWasmDatabaseError { - #[error(transparent)] - SerdeError(#[from] serde_wasm_bindgen::Error), - #[error("JavaScript error: {0:?}")] - JsError(JsValue), - #[error("Initialization pending")] - InitializationPending, - #[error("Initialization failed: {0}")] - InitializationFailed(String), -} - -impl From for SQLiteWasmDatabaseError { - fn from(value: JsValue) -> Self { - SQLiteWasmDatabaseError::JsError(value) - } -} - -impl From for JsValue { - fn from(value: SQLiteWasmDatabaseError) -> Self { - JsError::new(&value.to_string()).into() - } -} - -impl From for WasmEncodedError { - fn from(value: SQLiteWasmDatabaseError) -> Self { - WasmEncodedError { - msg: value.to_string(), - readable_msg: value.to_string(), - } - } -} - -#[wasm_export] -impl SQLiteWasmDatabase { - fn ensure_array(params: &JsValue) -> Result { - if params.is_undefined() || params.is_null() { - return Ok(js_sys::Array::new()); - } - if js_sys::Array::is_array(params) { - return Ok(params.clone().unchecked_into()); - } - Err(SQLiteWasmDatabaseError::JsError(JsValue::from_str( - "params must be an array", - ))) - } - /// Create a new database connection with fully embedded worker - #[wasm_export(js_name = "new", preserve_js_class)] - pub async fn new(db_name: &str) -> Result { - let db_name = db_name.trim(); - if db_name.is_empty() { - return Err(SQLiteWasmDatabaseError::JsError(JsValue::from_str( - "Database name is required", - ))); - } - let db = Self::construct(db_name)?; - db.wait_until_ready().await?; - Ok(db) - } - - fn construct(db_name: &str) -> Result { - let worker_code = generate_self_contained_worker(db_name); - let worker = create_worker_from_code(&worker_code)?; - - let pending_queries: Rc>> = - Rc::new(RefCell::new(HashMap::new())); - let ready_signal = ReadySignal::new(); - install_onmessage_handler(&worker, Rc::clone(&pending_queries), ready_signal.clone()); - let next_request_id = Rc::new(RefCell::new(1u32)); - - Ok(SQLiteWasmDatabase { - worker, - pending_queries, - next_request_id, - ready_signal, - }) - } - - fn normalize_params_js(params: &JsValue) -> Result { - let arr = Self::ensure_array(params)?; - (0..arr.length()).try_fold(js_sys::Array::new(), |normalized, i| { - let nv = normalize_one_param(&arr.get(i), i)?; - normalized.push(&nv); - Ok(normalized) - }) - } - - async fn wait_until_ready(&self) -> Result<(), SQLiteWasmDatabaseError> { - match self.ready_signal.current_state() { - InitializationState::Ready => return Ok(()), - InitializationState::Failed(reason) => { - return Err(SQLiteWasmDatabaseError::InitializationFailed(reason)); - } - InitializationState::Pending => {} - } - - let promise = self.ready_signal.wait_promise()?; - - match JsFuture::from(promise).await { - Ok(_) => match self.ready_signal.current_state() { - InitializationState::Ready => Ok(()), - InitializationState::Failed(reason) => { - Err(SQLiteWasmDatabaseError::InitializationFailed(reason)) - } - InitializationState::Pending => Err(SQLiteWasmDatabaseError::InitializationFailed( - "Worker failed to signal readiness".to_string(), - )), - }, - Err(err) => { - let reason = describe_js_value(&err); - self.ready_signal.mark_failed(reason.clone()); - Err(SQLiteWasmDatabaseError::InitializationFailed(reason)) - } - } - } - - /// Execute a SQL query (optionally parameterized via JS Array) - /// - /// Passing `undefined`/`null` from JS maps to `None`. - #[wasm_export(js_name = "query", unchecked_return_type = "string")] - pub async fn query( - &self, - sql: &str, - params: Option, - ) -> Result { - let worker = &self.worker; - let pending_queries = Rc::clone(&self.pending_queries); - let sql = sql.to_string(); - let params_js = params.map(JsValue::from).unwrap_or(JsValue::UNDEFINED); - let params_array = Self::normalize_params_js(¶ms_js)?; - - if let InitializationState::Failed(reason) = self.ready_signal.current_state() { - return Err(SQLiteWasmDatabaseError::InitializationFailed(reason)); - } - - // Build the message object up-front and propagate errors - let message = js_sys::Object::new(); - js_sys::Reflect::set( - &message, - &JsValue::from_str("type"), - &JsValue::from_str("execute-query"), - ) - .map_err(SQLiteWasmDatabaseError::JsError)?; - // Generate requestId and attach - let request_id = { - let mut n = self.next_request_id.borrow_mut(); - let id = *n; - *n = n.wrapping_add(1).max(1); // keep non-zero - id - }; - js_sys::Reflect::set( - &message, - &JsValue::from_str("requestId"), - &JsValue::from_f64(request_id as f64), - ) - .map_err(SQLiteWasmDatabaseError::JsError)?; - js_sys::Reflect::set( - &message, - &JsValue::from_str("sql"), - &JsValue::from_str(&sql), - ) - .map_err(SQLiteWasmDatabaseError::JsError)?; - if params_array.length() > 0 { - let params_js = JsValue::from(params_array.clone()); - js_sys::Reflect::set(&message, &JsValue::from_str("params"), ¶ms_js) - .map_err(SQLiteWasmDatabaseError::JsError)?; - } - - // Create the Promise that will resolve/reject when the worker responds - // Attempt to post the message first; only track callbacks on success. - 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 result = match JsFuture::from(promise).await { - Ok(value) => value, - Err(err) => { - if err - .as_string() - .as_deref() - .map(|s| s == "InitializationPending") - .unwrap_or(false) - { - return Err(SQLiteWasmDatabaseError::InitializationPending); - } - return Err(SQLiteWasmDatabaseError::JsError(err)); - } - }; - Ok(result.as_string().unwrap_or_else(|| format!("{result:?}"))) - } -} +pub use db::SQLiteWasmDatabase; +pub use errors::SQLiteWasmDatabaseError; #[cfg(all(test, target_family = "wasm"))] -mod tests { - use super::*; - use wasm_bindgen::JsCast; - use wasm_bindgen_test::*; - - wasm_bindgen_test_configure!(run_in_browser); - - fn make_control_message(msg_type: &str, error: Option<&str>) -> JsValue { - let msg = js_sys::Object::new(); - let _ = js_sys::Reflect::set( - &msg, - &JsValue::from_str("type"), - &JsValue::from_str(msg_type), - ); - if let Some(err) = error { - let _ = - js_sys::Reflect::set(&msg, &JsValue::from_str("error"), &JsValue::from_str(err)); - } - msg.into() - } - - #[wasm_bindgen_test] - fn test_sqlite_wasm_database_error_from_js_value() { - let js_error = JsValue::from_str("Test error message"); - let db_error = SQLiteWasmDatabaseError::from(js_error); - - match db_error { - SQLiteWasmDatabaseError::JsError(val) => { - assert_eq!(val.as_string().unwrap(), "Test error message"); - } - _ => panic!("Expected JsError variant"), - } - } - - #[wasm_bindgen_test] - fn test_sqlite_wasm_database_error_to_js_value() { - let db_error = SQLiteWasmDatabaseError::JsError(JsValue::from_str("Test error")); - let js_value = JsValue::from(db_error); - - assert!(js_value.is_object()); - } - - #[wasm_bindgen_test] - fn test_sqlite_wasm_database_error_to_wasm_encoded_error() { - let db_error = SQLiteWasmDatabaseError::JsError(JsValue::from_str("Test error")); - let wasm_error = WasmEncodedError::from(db_error); - - assert!(wasm_error.msg.contains("Test error")); - assert!(wasm_error.readable_msg.contains("Test error")); - } - - #[wasm_bindgen_test] - fn test_sqlite_wasm_database_error_display() { - let db_error = SQLiteWasmDatabaseError::JsError(JsValue::from_str("Display test")); - let error_string = format!("{db_error}"); - - assert!(error_string.contains("JavaScript error")); - } - - #[wasm_bindgen_test] - async fn test_sqlite_wasm_database_serialization() { - let db = SQLiteWasmDatabase::new("testdb") - .await - .expect("Should create database"); - let serialized = serde_json::to_string(&db); - - assert!(serialized.is_ok()); - let json_str = serialized.unwrap(); - assert_eq!(json_str, "{}"); // Empty struct should serialize to empty object - } - - #[wasm_bindgen_test] - async fn test_sqlite_wasm_database_creation() { - let result = SQLiteWasmDatabase::new("testdb").await; - - match result { - Ok(_db) => {} - Err(e) => { - let error_msg = format!("{e:?}"); - assert!(!error_msg.is_empty()); - } - } - } - - #[wasm_bindgen_test] - async fn test_query_message_format() { - if let Ok(db) = SQLiteWasmDatabase::new("testdb").await { - let result = db.query("SELECT 1", None).await; - - match result { - Ok(_) => {} - Err(e) => { - let error_msg = format!("{e:?}"); - assert!(!error_msg.is_empty()); - } - } - } - } - - #[wasm_bindgen_test] - fn test_error_propagation_chain() { - let serde_error = serde_wasm_bindgen::Error::new("Test serde error"); - let db_error = SQLiteWasmDatabaseError::SerdeError(serde_error); - - match db_error { - SQLiteWasmDatabaseError::SerdeError(_) => {} - _ => panic!("Expected SerdeError variant"), - } - - let js_value = JsValue::from(db_error); - assert!(js_value.is_object()); // Should convert to JS error object - } - - #[wasm_bindgen_test] - fn test_worker_template_generation() { - let worker_code = generate_self_contained_worker("testdb"); - - assert!(!worker_code.is_empty()); - assert!( - worker_code.contains("importScripts") - || worker_code.contains("import") - || worker_code.len() > 100 - ); - assert!( - worker_code.contains("__SQLITE_FOLLOWER_TIMEOUT_MS"), - "Worker template should embed follower timeout configuration" - ); - } - - #[wasm_bindgen_test(async)] - async fn test_ready_signal_resolves_on_worker_ready_msg() { - let signal = ReadySignal::new(); - let promise = signal - .wait_promise() - .expect("ready promise should exist before resolution"); - let future = JsFuture::from(promise); - - let message = make_control_message("worker-ready", None); - let handled = handle_worker_control_message(&message, &signal); - - assert!(handled, "Ready message should be handled"); - assert!( - matches!(signal.current_state(), InitializationState::Ready), - "Signal should transition to Ready state" - ); - let result = future.await; - assert!( - result.is_ok(), - "Ready promise should resolve successfully, got {result:?}" - ); - } - - #[wasm_bindgen_test(async)] - async fn test_ready_signal_rejects_on_worker_error_msg() { - let signal = ReadySignal::new(); - let promise = signal - .wait_promise() - .expect("ready promise should exist before resolution"); - let future = JsFuture::from(promise); - - let message = make_control_message("worker-error", Some("boom")); - let handled = handle_worker_control_message(&message, &signal); - - assert!(handled, "Error message should be handled"); - match signal.current_state() { - InitializationState::Failed(reason) => { - assert_eq!(reason, "boom", "Failure reason should match payload") - } - other => panic!("Expected Failed state, got {other:?}"), - } - let err = future - .await - .expect_err("Promise should reject on worker-error"); - assert_eq!( - err.as_string().as_deref(), - Some("boom"), - "Rejected value should propagate worker error text" - ); - } - - // --- Test helpers for spying on Worker.prototype.postMessage --- - fn install_post_message_spy() { - // Wrap Worker.prototype.postMessage to capture the message argument - // into a global so we can assert on the message content. - let code = r#" - (function(){ - try { - // Clear any previous message - self.__lastMessage = undefined; - if (!self.__origPostMessage) { - self.__origPostMessage = Worker.prototype.postMessage; - } - Worker.prototype.postMessage = function(msg) { - self.__lastMessage = msg; - return self.__origPostMessage.call(this, msg); - }; - } catch (e) { - // If Worker not available, tests will still exercise normalization paths - } - })() - "#; - let f = js_sys::Function::new_no_args(code); - let _ = f.call0(&JsValue::UNDEFINED); - } - - fn uninstall_post_message_spy() { - let code = r#" - (function(){ - try { - if (self.__origPostMessage) { - Worker.prototype.postMessage = self.__origPostMessage; - self.__origPostMessage = undefined; - } - } catch (e) { - } - })() - "#; - let f = js_sys::Function::new_no_args(code); - let _ = f.call0(&JsValue::UNDEFINED); - } - - fn take_last_message() -> Option { - let global = js_sys::global(); - let key = JsValue::from_str("__lastMessage"); - let val = js_sys::Reflect::get(&global, &key).ok(); - // Clear for next usage - let _ = js_sys::Reflect::set(&global, &key, &JsValue::UNDEFINED); - val.and_then(|v| if v.is_undefined() { None } else { Some(v) }) - } - - #[wasm_bindgen_test] - async fn test_query_with_various_param_types_and_normalization() { - install_post_message_spy(); - let db = SQLiteWasmDatabase::new("testdb").await.expect("db created"); - - // Build a params array with all requested JS value types - let arr = js_sys::Array::new(); - // number - arr.push(&JsValue::from_f64(42.0)); - // string - arr.push(&JsValue::from_str("hello")); - // boolean - arr.push(&JsValue::from_bool(true)); - // null - arr.push(&JsValue::NULL); - // Create a sparse hole at index 4 (left unset, should normalize to NULL) - // Then set BigInt at index 5 to keep subsequent indices consistent - let bi = js_sys::BigInt::from(1234u32); - let bi_js: JsValue = bi.into(); - js_sys::Reflect::set(&arr, &JsValue::from_f64(5.0), &bi_js).expect("set index 5"); - // Uint8Array - let bytes: [u8; 3] = [1, 2, 3]; - let u8 = js_sys::Uint8Array::from(&bytes[..]); - let u8_js: JsValue = u8.into(); - arr.push(&u8_js); - // ArrayBuffer - let buf = js_sys::ArrayBuffer::new(4); - let typed = js_sys::Uint8Array::new(&buf); - typed.copy_from(&[5, 6, 7, 8]); - let buf_js: JsValue = buf.into(); - arr.push(&buf_js); - - // Call query; we don't care about result success here, just that - // normalization and message construction do not panic. - let _ = db.query("SELECT 1", Some(arr)).await; - - // Inspect the last posted message - if let Some(msg) = take_last_message() { - // type - let ty = js_sys::Reflect::get(&msg, &JsValue::from_str("type")).unwrap(); - assert_eq!(ty.as_string().as_deref(), Some("execute-query")); - // sql - let sql = js_sys::Reflect::get(&msg, &JsValue::from_str("sql")).unwrap(); - assert_eq!(sql.as_string().as_deref(), Some("SELECT 1")); - // params presence - let has_params = - js_sys::Reflect::has(&msg, &JsValue::from_str("params")).unwrap_or(false); - assert!(has_params, "params should be present for non-empty array"); - - let params = js_sys::Reflect::get(&msg, &JsValue::from_str("params")).unwrap(); - assert!(js_sys::Array::is_array(¶ms)); - let params: js_sys::Array = params.unchecked_into(); - assert_eq!(params.length(), 8); - - // number - let v0 = params.get(0); - assert_eq!(v0.as_f64(), Some(42.0)); - // string - let v1 = params.get(1); - assert_eq!(v1.as_string().as_deref(), Some("hello")); - // boolean - let v2 = params.get(2); - assert_eq!(v2.as_bool(), Some(true)); - // null - let v3 = params.get(3); - assert!(v3.is_null()); - // sparse hole mapped to null - let v4 = params.get(4); - assert!(v4.is_null()); - // BigInt encoded object { __type: "bigint", value: string } - let v5 = params.get(5); - assert!(v5.is_object()); - let t5 = js_sys::Reflect::get(&v5, &JsValue::from_str("__type")).unwrap(); - assert_eq!(t5.as_string().as_deref(), Some("bigint")); - let val5 = js_sys::Reflect::get(&v5, &JsValue::from_str("value")).unwrap(); - assert_eq!(val5.as_string().as_deref(), Some("1234")); - // Uint8Array encoded object { __type: "blob", base64 } - let v6 = params.get(6); - assert!(v6.is_object()); - let t6 = js_sys::Reflect::get(&v6, &JsValue::from_str("__type")).unwrap(); - assert_eq!(t6.as_string().as_deref(), Some("blob")); - let b64_6 = js_sys::Reflect::get(&v6, &JsValue::from_str("base64")).unwrap(); - let b64_6 = b64_6.as_string().expect("base64 string"); - let expected_6 = base64::engine::general_purpose::STANDARD.encode([1u8, 2, 3]); - assert_eq!(b64_6, expected_6); - // ArrayBuffer encoded object { __type: "blob", base64 } - let v7 = params.get(7); - assert!(v7.is_object()); - let t7 = js_sys::Reflect::get(&v7, &JsValue::from_str("__type")).unwrap(); - assert_eq!(t7.as_string().as_deref(), Some("blob")); - let b64_7 = js_sys::Reflect::get(&v7, &JsValue::from_str("base64")).unwrap(); - let b64_7 = b64_7.as_string().expect("base64 string"); - let expected_7 = base64::engine::general_purpose::STANDARD.encode([5u8, 6, 7, 8]); - assert_eq!(b64_7, expected_7); - } else { - // If we failed to capture a message, at least the call returned - // without crashing via normalization. Nothing to assert here. - } - - uninstall_post_message_spy(); - } - - #[wasm_bindgen_test] - async fn test_query_params_presence_empty_array_vs_none() { - install_post_message_spy(); - let db = SQLiteWasmDatabase::new("testdb").await.expect("db created"); - - // Case: None -> no params property on message - let _ = db.query("SELECT 1", None).await; - if let Some(msg) = take_last_message() { - let has_params = - js_sys::Reflect::has(&msg, &JsValue::from_str("params")).unwrap_or(true); - assert!(!has_params, "params should be absent when params=None"); - } - - // Case: empty array -> also no params property - let empty = js_sys::Array::new(); - let _ = db.query("SELECT 1", Some(empty)).await; - if let Some(msg) = take_last_message() { - let has_params = - js_sys::Reflect::has(&msg, &JsValue::from_str("params")).unwrap_or(true); - assert!(!has_params, "params should be absent for empty array"); - } - - // Case: one param -> params present - let one = js_sys::Array::new(); - one.push(&JsValue::from_f64(1.0)); - let _ = db.query("SELECT 1", Some(one)).await; - if let Some(msg) = take_last_message() { - let has_params = - js_sys::Reflect::has(&msg, &JsValue::from_str("params")).unwrap_or(false); - assert!(has_params, "params should be present for non-empty array"); - } - - uninstall_post_message_spy(); - } - - #[wasm_bindgen_test] - async fn test_query_rejects_nan_infinity_params() { - let db = SQLiteWasmDatabase::new("testdb").await.expect("db created"); - - // NaN - { - let arr = js_sys::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"); - } - - // +Infinity - { - let arr = js_sys::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"); - } - - // -Infinity - { - let arr = js_sys::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"); - } - } -} +mod tests; diff --git a/packages/sqlite-web/src/params.rs b/packages/sqlite-web/src/params.rs new file mode 100644 index 0000000..e703358 --- /dev/null +++ b/packages/sqlite-web/src/params.rs @@ -0,0 +1,92 @@ +use base64::Engine; +use js_sys::{Array, ArrayBuffer, BigInt, Object, Reflect, Uint8Array}; +use wasm_bindgen::prelude::*; + +use crate::errors::SQLiteWasmDatabaseError; + +pub(crate) fn normalize_params_js(params: &JsValue) -> Result { + let arr = ensure_array(params)?; + (0..arr.length()).try_fold(Array::new(), |normalized, i| { + let nv = normalize_one_param(&arr.get(i), i)?; + normalized.push(&nv); + Ok(normalized) + }) +} + +fn ensure_array(params: &JsValue) -> Result { + if params.is_undefined() || params.is_null() { + return Ok(Array::new()); + } + if Array::is_array(params) { + return Ok(params.clone().unchecked_into()); + } + Err(SQLiteWasmDatabaseError::JsError(JsValue::from_str( + "params must be an array", + ))) +} + +fn normalize_one_param(v: &JsValue, index: u32) -> Result { + if v.is_null() || v.is_undefined() { + return Ok(JsValue::NULL); + } + if let Ok(bi) = v.clone().dyn_into::() { + return encode_bigint_to_obj(bi); + } + if let Ok(typed) = v.clone().dyn_into::() { + return encode_binary_to_obj(typed.to_vec()); + } + if let Ok(buf) = v.clone().dyn_into::() { + let typed = Uint8Array::new(&buf); + return encode_binary_to_obj(typed.to_vec()); + } + if let Some(n) = v.as_f64() { + if !n.is_finite() { + return Err(SQLiteWasmDatabaseError::JsError(JsValue::from_str( + &format!( + "Invalid numeric value at position {} (NaN/Infinity not supported.)", + index + 1 + ), + ))); + } + return Ok(JsValue::from_f64(n)); + } + if let Some(b) = v.as_bool() { + return Ok(JsValue::from_bool(b)); + } + if let Some(s) = v.as_string() { + return Ok(JsValue::from_str(&s)); + } + Err(SQLiteWasmDatabaseError::JsError(JsValue::from_str( + &format!("Unsupported parameter type at position {}", index + 1), + ))) +} + +fn encode_bigint_to_obj(bi: BigInt) -> Result { + let obj = Object::new(); + let s = bi + .to_string(10) + .map_err(|e| SQLiteWasmDatabaseError::JsError(e.into()))?; + Reflect::set( + &obj, + &JsValue::from_str("__type"), + &JsValue::from_str("bigint"), + ) + .map_err(SQLiteWasmDatabaseError::from)?; + Reflect::set(&obj, &JsValue::from_str("value"), &JsValue::from(s)) + .map_err(SQLiteWasmDatabaseError::from)?; + Ok(obj.into()) +} + +fn encode_binary_to_obj(bytes: Vec) -> Result { + let b64 = base64::engine::general_purpose::STANDARD.encode(bytes); + let obj = Object::new(); + Reflect::set( + &obj, + &JsValue::from_str("__type"), + &JsValue::from_str("blob"), + ) + .map_err(SQLiteWasmDatabaseError::from)?; + Reflect::set(&obj, &JsValue::from_str("base64"), &JsValue::from_str(&b64)) + .map_err(SQLiteWasmDatabaseError::from)?; + Ok(obj.into()) +} diff --git a/packages/sqlite-web/src/ready.rs b/packages/sqlite-web/src/ready.rs new file mode 100644 index 0000000..016a5aa --- /dev/null +++ b/packages/sqlite-web/src/ready.rs @@ -0,0 +1,94 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use js_sys::{Function, Promise}; +use wasm_bindgen::prelude::*; + +use crate::errors::SQLiteWasmDatabaseError; + +#[derive(Debug, Clone)] +pub(crate) enum InitializationState { + Pending, + Ready, + Failed(String), +} + +#[derive(Clone)] +pub(crate) struct ReadySignal { + state: Rc>, + resolve: Rc>>, + reject: Rc>>, + promise: Rc>>, +} + +impl ReadySignal { + pub(crate) fn new() -> Self { + let state = Rc::new(RefCell::new(InitializationState::Pending)); + let resolve = Rc::new(RefCell::new(None)); + let reject = Rc::new(RefCell::new(None)); + let promise = Rc::new(RefCell::new(None)); + { + let ready_promise = create_ready_promise(&resolve, &reject); + promise.borrow_mut().replace(ready_promise); + } + Self { + state, + resolve, + reject, + promise, + } + } + + pub(crate) fn current_state(&self) -> InitializationState { + self.state.borrow().clone() + } + + pub(crate) fn wait_promise(&self) -> Result { + self.promise.borrow().as_ref().cloned().ok_or_else(|| { + SQLiteWasmDatabaseError::InitializationFailed( + "Worker readiness promise missing".to_string(), + ) + }) + } + + pub(crate) fn mark_ready(&self) { + { + let mut state = self.state.borrow_mut(); + if matches!(*state, InitializationState::Ready) { + return; + } + *state = InitializationState::Ready; + } + if let Some(resolve) = self.resolve.borrow_mut().take() { + let _ = resolve.call0(&JsValue::NULL); + } + self.reject.borrow_mut().take(); + self.promise.borrow_mut().take(); + } + + pub(crate) fn mark_failed(&self, reason: String) { + { + let mut state = self.state.borrow_mut(); + if !matches!(&*state, InitializationState::Failed(existing) if existing == &reason) { + *state = InitializationState::Failed(reason.clone()); + } + } + self.resolve.borrow_mut().take(); + if let Some(reject) = self.reject.borrow_mut().take() { + let _ = reject.call1(&JsValue::NULL, &JsValue::from_str(&reason)); + } + self.promise.borrow_mut().take(); + } +} + +fn create_ready_promise( + resolve_cell: &Rc>>, + reject_cell: &Rc>>, +) -> Promise { + let resolve_clone = Rc::clone(resolve_cell); + let reject_clone = Rc::clone(reject_cell); + Promise::new(&mut move |resolve, reject| { + resolve_clone.borrow_mut().replace(resolve); + reject_clone.borrow_mut().replace(reject); + }) +} diff --git a/packages/sqlite-web/src/tests.rs b/packages/sqlite-web/src/tests.rs new file mode 100644 index 0000000..2365b28 --- /dev/null +++ b/packages/sqlite-web/src/tests.rs @@ -0,0 +1,352 @@ +#![cfg(all(test, target_family = "wasm"))] + +use crate::ready::{InitializationState, ReadySignal}; +use crate::worker::handle_worker_control_message; +use crate::worker_template::generate_self_contained_worker; +use crate::{SQLiteWasmDatabase, SQLiteWasmDatabaseError}; + +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_test::*; + +use base64::Engine; +use wasm_bindgen_utils::prelude::{serde_wasm_bindgen, WasmEncodedError}; + +wasm_bindgen_test_configure!(run_in_browser); + +fn make_control_message(msg_type: &str, error: Option<&str>) -> JsValue { + let msg = js_sys::Object::new(); + let _ = js_sys::Reflect::set( + &msg, + &JsValue::from_str("type"), + &JsValue::from_str(msg_type), + ); + if let Some(err) = error { + let _ = js_sys::Reflect::set(&msg, &JsValue::from_str("error"), &JsValue::from_str(err)); + } + msg.into() +} + +#[wasm_bindgen_test] +fn test_sqlite_wasm_database_error_from_js_value() { + let js_error = JsValue::from_str("Test error message"); + let db_error = SQLiteWasmDatabaseError::from(js_error); + + match db_error { + SQLiteWasmDatabaseError::JsError(val) => { + assert_eq!(val.as_string().unwrap(), "Test error message"); + } + _ => panic!("Expected JsError variant"), + } +} + +#[wasm_bindgen_test] +fn test_sqlite_wasm_database_error_to_js_value() { + let db_error = SQLiteWasmDatabaseError::JsError(JsValue::from_str("Test error")); + let js_value = JsValue::from(db_error); + + assert!(js_value.is_object()); +} + +#[wasm_bindgen_test] +fn test_sqlite_wasm_database_error_to_wasm_encoded_error() { + let db_error = SQLiteWasmDatabaseError::JsError(JsValue::from_str("Test error")); + let wasm_error = WasmEncodedError::from(db_error); + + assert!(wasm_error.msg.contains("Test error")); + assert!(wasm_error.readable_msg.contains("Test error")); +} + +#[wasm_bindgen_test] +fn test_sqlite_wasm_database_error_display() { + let db_error = SQLiteWasmDatabaseError::JsError(JsValue::from_str("Display test")); + let error_string = format!("{db_error}"); + + assert!(error_string.contains("JavaScript error")); +} + +#[wasm_bindgen_test] +async fn test_sqlite_wasm_database_serialization() { + let db = SQLiteWasmDatabase::new("testdb") + .await + .expect("Should create database"); + let serialized = serde_json::to_string(&db); + + assert!(serialized.is_ok()); + let json_str = serialized.unwrap(); + assert_eq!(json_str, "{}"); +} + +#[wasm_bindgen_test] +async fn test_sqlite_wasm_database_creation() { + let result = SQLiteWasmDatabase::new("testdb").await; + + match result { + Ok(_db) => {} + Err(e) => { + let error_msg = format!("{e:?}"); + assert!(!error_msg.is_empty()); + } + } +} + +#[wasm_bindgen_test] +async fn test_query_message_format() { + if let Ok(db) = SQLiteWasmDatabase::new("testdb").await { + let result = db.query("SELECT 1", None).await; + + match result { + Ok(_) => {} + Err(e) => { + let error_msg = format!("{e:?}"); + assert!(!error_msg.is_empty()); + } + } + } +} + +#[wasm_bindgen_test] +fn test_error_propagation_chain() { + let serde_error = serde_wasm_bindgen::Error::new("Test serde error"); + let db_error = SQLiteWasmDatabaseError::SerdeError(serde_error); + + match db_error { + SQLiteWasmDatabaseError::SerdeError(_) => {} + _ => panic!("Expected SerdeError variant"), + } + + let js_value = JsValue::from(db_error); + assert!(js_value.is_object()); +} + +#[wasm_bindgen_test] +fn test_worker_template_generation() { + let worker_code = generate_self_contained_worker("testdb"); + + assert!(!worker_code.is_empty()); + assert!( + worker_code.contains("importScripts") + || worker_code.contains("import") + || worker_code.len() > 100 + ); + assert!( + worker_code.contains("__SQLITE_FOLLOWER_TIMEOUT_MS"), + "Worker template should embed follower timeout configuration" + ); +} + +#[wasm_bindgen_test(async)] +async fn test_ready_signal_resolves_on_worker_ready_msg() { + let signal = ReadySignal::new(); + let promise = signal + .wait_promise() + .expect("ready promise should exist before resolution"); + let future = wasm_bindgen_futures::JsFuture::from(promise); + + let message = make_control_message("worker-ready", None); + let handled = handle_worker_control_message(&message, &signal); + + assert!(handled, "Ready message should be handled"); + assert!( + matches!(signal.current_state(), InitializationState::Ready), + "Signal should transition to Ready state" + ); + let result = future.await; + assert!(result.is_ok(), "Ready promise should resolve successfully"); +} + +#[wasm_bindgen_test(async)] +async fn test_ready_signal_rejects_on_worker_error_msg() { + let signal = ReadySignal::new(); + let promise = signal + .wait_promise() + .expect("ready promise should exist before resolution"); + let future = wasm_bindgen_futures::JsFuture::from(promise); + + let message = make_control_message("worker-error", Some("boom")); + let handled = handle_worker_control_message(&message, &signal); + + assert!(handled, "Error message should be handled"); + match signal.current_state() { + InitializationState::Failed(reason) => { + assert_eq!(reason, "boom", "Failure reason should match payload") + } + other => panic!("Expected Failed state, got {other:?}"), + } + let err = future + .await + .expect_err("Promise should reject on worker-error"); + assert_eq!( + err.as_string().as_deref(), + Some("boom"), + "Rejected value should propagate worker error text" + ); +} + +#[wasm_bindgen_test] +async fn test_query_with_various_param_types_and_normalization() { + install_post_message_spy(); + let db = SQLiteWasmDatabase::new("testdb").await.expect("db created"); + + let arr = js_sys::Array::new(); + arr.push(&JsValue::from_f64(42.0)); + arr.push(&JsValue::from_str("hello")); + arr.push(&JsValue::from_bool(true)); + arr.push(&JsValue::NULL); + let bi = js_sys::BigInt::from(1234u32); + let bi_js: JsValue = bi.into(); + js_sys::Reflect::set(&arr, &JsValue::from_f64(5.0), &bi_js).expect("set index 5"); + let bytes: [u8; 3] = [1, 2, 3]; + let u8 = js_sys::Uint8Array::from(&bytes[..]); + let u8_js: JsValue = u8.into(); + arr.push(&u8_js); + let buf = js_sys::ArrayBuffer::new(4); + let typed = js_sys::Uint8Array::new(&buf); + typed.copy_from(&[5, 6, 7, 8]); + let buf_js: JsValue = buf.into(); + arr.push(&buf_js); + + let _ = db.query("SELECT 1", Some(arr)).await; + + if let Some(msg) = take_last_message() { + let ty = js_sys::Reflect::get(&msg, &JsValue::from_str("type")).unwrap(); + assert_eq!(ty.as_string().as_deref(), Some("execute-query")); + let sql = js_sys::Reflect::get(&msg, &JsValue::from_str("sql")).unwrap(); + assert_eq!(sql.as_string().as_deref(), Some("SELECT 1")); + let has_params = js_sys::Reflect::has(&msg, &JsValue::from_str("params")).unwrap_or(false); + assert!(has_params, "params should be present for non-empty array"); + + let params = js_sys::Reflect::get(&msg, &JsValue::from_str("params")).unwrap(); + assert!(js_sys::Array::is_array(¶ms)); + let params: js_sys::Array = params.unchecked_into(); + assert_eq!(params.length(), 8); + + let v0 = params.get(0); + assert_eq!(v0.as_f64(), Some(42.0)); + let v1 = params.get(1); + assert_eq!(v1.as_string().as_deref(), Some("hello")); + let v2 = params.get(2); + assert_eq!(v2.as_bool(), Some(true)); + let v3 = params.get(3); + assert!(v3.is_null()); + let v4 = params.get(4); + assert!(v4.is_null()); + let v5 = params.get(5); + assert!(v5.is_object()); + let t5 = js_sys::Reflect::get(&v5, &JsValue::from_str("__type")).unwrap(); + assert_eq!(t5.as_string().as_deref(), Some("bigint")); + let val5 = js_sys::Reflect::get(&v5, &JsValue::from_str("value")).unwrap(); + assert_eq!(val5.as_string().as_deref(), Some("1234")); + let v6 = params.get(6); + assert!(v6.is_object()); + let t6 = js_sys::Reflect::get(&v6, &JsValue::from_str("__type")).unwrap(); + assert_eq!(t6.as_string().as_deref(), Some("blob")); + let b64_6 = js_sys::Reflect::get(&v6, &JsValue::from_str("base64")).unwrap(); + let b64_6 = b64_6.as_string().expect("base64 string"); + let expected_6 = base64::engine::general_purpose::STANDARD.encode([1u8, 2, 3]); + assert_eq!(b64_6, expected_6); + let v7 = params.get(7); + assert!(v7.is_object()); + let t7 = js_sys::Reflect::get(&v7, &JsValue::from_str("__type")).unwrap(); + assert_eq!(t7.as_string().as_deref(), Some("blob")); + let b64_7 = js_sys::Reflect::get(&v7, &JsValue::from_str("base64")).unwrap(); + let b64_7 = b64_7.as_string().expect("base64 string"); + let expected_7 = base64::engine::general_purpose::STANDARD.encode([5u8, 6, 7, 8]); + assert_eq!(b64_7, expected_7); + } + + uninstall_post_message_spy(); +} + +#[wasm_bindgen_test] +async fn test_query_params_presence_empty_array_vs_none() { + install_post_message_spy(); + let db = SQLiteWasmDatabase::new("testdb").await.expect("db created"); + + let _ = db.query("SELECT 1", None).await; + if let Some(msg) = take_last_message() { + let has_params = js_sys::Reflect::has(&msg, &JsValue::from_str("params")).unwrap_or(true); + assert!(!has_params, "params should be absent when params=None"); + } + + let empty = js_sys::Array::new(); + let _ = db.query("SELECT 1", Some(empty)).await; + if let Some(msg) = take_last_message() { + let has_params = js_sys::Reflect::has(&msg, &JsValue::from_str("params")).unwrap_or(true); + assert!(!has_params, "params should be absent for empty array"); + } + + let one = js_sys::Array::new(); + one.push(&JsValue::from_f64(1.0)); + let _ = db.query("SELECT 1", Some(one)).await; + if let Some(msg) = take_last_message() { + let has_params = js_sys::Reflect::has(&msg, &JsValue::from_str("params")).unwrap_or(false); + assert!(has_params, "params should be present for non-empty array"); + } + + uninstall_post_message_spy(); +} + +#[wasm_bindgen_test] +async fn test_query_rejects_nan_infinity_params() { + let db = SQLiteWasmDatabase::new("testdb").await.expect("db created"); + + let arr = js_sys::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 = js_sys::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 = js_sys::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"); +} + +fn install_post_message_spy() { + let code = r#" + (function(){ + try { + self.__lastMessage = undefined; + if (!self.__origPostMessage) { + self.__origPostMessage = Worker.prototype.postMessage; + } + Worker.prototype.postMessage = function(msg) { + self.__lastMessage = msg; + return self.__origPostMessage.call(this, msg); + }; + } catch (e) { + } + })() + "#; + let f = js_sys::Function::new_no_args(code); + let _ = f.call0(&JsValue::UNDEFINED); +} + +fn uninstall_post_message_spy() { + let code = r#" + (function(){ + try { + if (self.__origPostMessage) { + Worker.prototype.postMessage = self.__origPostMessage; + self.__origPostMessage = undefined; + } + } catch (e) { + } + })() + "#; + let f = js_sys::Function::new_no_args(code); + let _ = f.call0(&JsValue::UNDEFINED); +} + +fn take_last_message() -> Option { + let global = js_sys::global(); + let key = JsValue::from_str("__lastMessage"); + let val = js_sys::Reflect::get(&global, &key).ok(); + let _ = js_sys::Reflect::set(&global, &key, &JsValue::UNDEFINED); + val.and_then(|v| if v.is_undefined() { None } else { Some(v) }) +} diff --git a/packages/sqlite-web/src/utils.rs b/packages/sqlite-web/src/utils.rs new file mode 100644 index 0000000..dcea7f7 --- /dev/null +++ b/packages/sqlite-web/src/utils.rs @@ -0,0 +1,14 @@ +use wasm_bindgen::prelude::*; + +pub(crate) fn describe_js_value(value: &JsValue) -> String { + if let Some(s) = value.as_string() { + return s; + } + if let Some(n) = value.as_f64() { + if n.fract() == 0.0 { + return format!("{n:.0}"); + } + return format!("{n}"); + } + format!("{value:?}") +} diff --git a/packages/sqlite-web/src/worker.rs b/packages/sqlite-web/src/worker.rs new file mode 100644 index 0000000..9f57af6 --- /dev/null +++ b/packages/sqlite-web/src/worker.rs @@ -0,0 +1,114 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + +use crate::ready::ReadySignal; +use crate::utils::describe_js_value; +use js_sys::{Array, Function, Reflect}; +use serde::Deserialize; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_utils::prelude::serde_wasm_bindgen; +use web_sys::{Blob, BlobPropertyBag, MessageEvent, Url, Worker}; + +pub(crate) fn create_worker_from_code(worker_code: &str) -> Result { + let blob_parts = Array::new(); + blob_parts.push(&JsValue::from_str(worker_code)); + + let blob_options = BlobPropertyBag::new(); + blob_options.set_type("application/javascript"); + + let blob = Blob::new_with_str_sequence_and_options(&blob_parts, &blob_options)?; + let worker_url = Url::create_object_url_with_blob(&blob)?; + let worker_res = Worker::new(&worker_url); + Url::revoke_object_url(&worker_url)?; + worker_res +} + +pub(crate) fn install_onmessage_handler( + worker: &Worker, + pending_queries: Rc>>, + ready_signal: ReadySignal, +) { + let pending_queries_clone = Rc::clone(&pending_queries); + let ready_signal_clone = ready_signal.clone(); + let onmessage = Closure::wrap(Box::new(move |event: MessageEvent| { + let data = event.data(); + if handle_worker_control_message(&data, &ready_signal_clone) { + return; + } + handle_query_result_message(&data, &pending_queries_clone); + }) as Box); + + worker.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + onmessage.forget(); +} + +#[derive(Deserialize)] +#[serde(tag = "type")] +enum WorkerControlMessage { + #[serde(rename = "worker-ready")] + Ready, + #[serde(rename = "worker-error")] + Error, +} + +pub(crate) fn handle_worker_control_message(data: &JsValue, ready_signal: &ReadySignal) -> bool { + match serde_wasm_bindgen::from_value::(data.clone()) { + Ok(WorkerControlMessage::Ready) => { + ready_signal.mark_ready(); + true + } + Ok(WorkerControlMessage::Error) => { + let reason = Reflect::get(data, &JsValue::from_str("error")) + .ok() + .filter(|val| !val.is_null() && !val.is_undefined()) + .map(|val| describe_js_value(&val)) + .unwrap_or_else(|| "Unknown worker error".to_string()); + ready_signal.mark_failed(reason); + true + } + Err(_) => false, + } +} + +fn handle_query_result_message( + data: &JsValue, + pending_queries: &Rc>>, +) { + let msg_type = Reflect::get(data, &JsValue::from_str("type")) + .ok() + .and_then(|obj| obj.as_string()); + + let Some(msg_type) = msg_type else { return }; + if msg_type != "query-result" { + return; + } + + let req_id_js = Reflect::get(data, &JsValue::from_str("requestId")).ok(); + let req_id = req_id_js.and_then(|v| v.as_f64()).map(|n| n as u32); + let Some(request_id) = req_id else { return }; + let entry = pending_queries.borrow_mut().remove(&request_id); + let Some((resolve, reject)) = entry else { + return; + }; + + let error = Reflect::get(data, &JsValue::from_str("error")) + .ok() + .filter(|e| !e.is_null() && !e.is_undefined()); + + if let Some(error) = error { + let error_str = error.as_string().unwrap_or_else(|| format!("{error:?}")); + let _ = reject.call1(&JsValue::NULL, &JsValue::from_str(&error_str)); + return; + } + + if let Some(result) = Reflect::get(data, &JsValue::from_str("result")) + .ok() + .filter(|r| !r.is_null() && !r.is_undefined()) + { + let result_str = result.as_string().unwrap_or_else(|| format!("{result:?}")); + let _ = resolve.call1(&JsValue::NULL, &JsValue::from_str(&result_str)); + } +} diff --git a/svelte-test/package-lock.json b/svelte-test/package-lock.json index 2130be8..85ada02 100644 --- a/svelte-test/package-lock.json +++ b/svelte-test/package-lock.json @@ -914,7 +914,7 @@ "node_modules/@rainlanguage/sqlite-web": { "version": "0.0.1-alpha.7", "resolved": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.7.tgz", - "integrity": "sha512-d+tuD5keEVZu2U8qGU1AHGTiVDLKVA8O4ei6JxPciv5WiZEFZ605KH6rSClUogkPOOBl7fcnhlKouiOojBKnYg==" + "integrity": "sha512-wq6GlwbKYEn8pY8m63mcenYOOesJLCfsduF7JojWj7prIg5dN+gZw0zfam8rI1N1elzpm3nbfvgYGJRpsdkJqg==" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.53.2", From d89d90f8a99eac232e1f138a1a99505ee4c1758e Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 10 Nov 2025 15:01:02 +0100 Subject: [PATCH 11/15] add tests to all the files --- packages/sqlite-web/src/db.rs | 89 +++++++++++++ packages/sqlite-web/src/errors.rs | 52 ++++++++ packages/sqlite-web/src/params.rs | 89 +++++++++++++ packages/sqlite-web/src/ready.rs | 45 +++++++ packages/sqlite-web/src/utils.rs | 29 +++++ packages/sqlite-web/src/worker.rs | 137 +++++++++++++++++++++ packages/sqlite-web/src/worker_template.rs | 31 +++++ 7 files changed, 472 insertions(+) diff --git a/packages/sqlite-web/src/db.rs b/packages/sqlite-web/src/db.rs index b013541..8349ce7 100644 --- a/packages/sqlite-web/src/db.rs +++ b/packages/sqlite-web/src/db.rs @@ -183,3 +183,92 @@ impl SQLiteWasmDatabase { Ok(result.as_string().unwrap_or_else(|| format!("{result:?}"))) } } + +#[cfg(all(test, target_family = "wasm"))] +mod tests { + use super::*; + use base64::Engine; + use js_sys::{Array, ArrayBuffer, BigInt, Uint8Array}; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[wasm_bindgen_test] + fn normalize_params_handles_none_and_empty_arrays() { + let empty = SQLiteWasmDatabase::normalize_params(None).expect("None => empty array"); + assert_eq!(empty.length(), 0); + + let arr = Array::new(); + let normalized = + SQLiteWasmDatabase::normalize_params(Some(arr)).expect("empty array stays empty"); + assert_eq!(normalized.length(), 0); + } + + #[wasm_bindgen_test] + fn normalize_params_normalizes_mixed_values() { + let params = Array::new(); + params.push(&JsValue::from_f64(123.0)); + params.push(&JsValue::from_str("hey")); + params.push(&JsValue::from_bool(true)); + params.push(&JsValue::NULL); + let bi: JsValue = BigInt::from(77u32).into(); + params.push(&bi); + let buf = ArrayBuffer::new(2); + Uint8Array::new(&buf).copy_from(&[9u8, 10]); + let buf_js: JsValue = buf.into(); + params.push(&buf_js); + + let normalized = + SQLiteWasmDatabase::normalize_params(Some(params)).expect("normalization works"); + assert_eq!(normalized.length(), 6); + assert_eq!(normalized.get(0).as_f64(), Some(123.0)); + assert_eq!(normalized.get(1).as_string().as_deref(), Some("hey")); + assert_eq!(normalized.get(2).as_bool(), Some(true)); + assert!(normalized.get(3).is_null()); + + let bigint = normalized.get(4); + assert_eq!( + js_sys::Reflect::get(&bigint, &JsValue::from_str("__type")) + .unwrap() + .as_string() + .as_deref(), + Some("bigint") + ); + assert_eq!( + js_sys::Reflect::get(&bigint, &JsValue::from_str("value")) + .unwrap() + .as_string() + .as_deref(), + Some("77") + ); + + let blob = normalized.get(5); + assert_eq!( + js_sys::Reflect::get(&blob, &JsValue::from_str("__type")) + .unwrap() + .as_string() + .as_deref(), + Some("blob") + ); + let actual = js_sys::Reflect::get(&blob, &JsValue::from_str("base64")) + .unwrap() + .as_string() + .expect("base64 string present"); + let expected = base64::engine::general_purpose::STANDARD.encode([9u8, 10]); + assert_eq!(actual, expected); + } + + #[wasm_bindgen_test(async)] + async fn new_rejects_blank_database_name() { + let err = match SQLiteWasmDatabase::new(" ").await { + Ok(_) => panic!("blank names should be rejected before constructing worker"), + Err(err) => err, + }; + match err { + SQLiteWasmDatabaseError::JsError(js) => { + assert_eq!(js.as_string().as_deref(), Some("Database name is required")) + } + other => panic!("expected JsError, got {other:?}"), + } + } +} diff --git a/packages/sqlite-web/src/errors.rs b/packages/sqlite-web/src/errors.rs index c1c94ff..f0a924d 100644 --- a/packages/sqlite-web/src/errors.rs +++ b/packages/sqlite-web/src/errors.rs @@ -34,3 +34,55 @@ impl From for WasmEncodedError { } } } + +#[cfg(all(test, target_family = "wasm"))] +mod tests { + use super::*; + use wasm_bindgen_test::*; + use wasm_bindgen_utils::prelude::serde_wasm_bindgen; + + wasm_bindgen_test_configure!(run_in_browser); + + #[wasm_bindgen_test] + fn js_value_into_error_variant() { + let js_val = JsValue::from_str("boom"); + match SQLiteWasmDatabaseError::from(js_val) { + SQLiteWasmDatabaseError::JsError(inner) => { + assert_eq!(inner.as_string().as_deref(), Some("boom")) + } + other => panic!("expected JsError, got {other:?}"), + } + } + + #[wasm_bindgen_test] + fn error_round_trips_back_into_js_value() { + let err = SQLiteWasmDatabaseError::InitializationFailed("nope".into()); + let js: JsValue = err.into(); + assert!(js.is_object()); + let error_obj = js_sys::Error::from(js); + let message = error_obj.message().as_string().unwrap_or_default(); + assert!( + message.contains("Initialization failed"), + "message should propagate initialization failure cause" + ); + } + + #[wasm_bindgen_test] + fn wasm_encoded_error_keeps_message_human_readable() { + let err = SQLiteWasmDatabaseError::InitializationPending; + let wasm_err = WasmEncodedError::from(err); + assert!(wasm_err.msg.contains("Initialization pending")); + assert!(wasm_err.readable_msg.contains("Initialization pending")); + } + + #[wasm_bindgen_test] + fn serde_error_variant_is_detectable() { + let serde_err = serde_wasm_bindgen::Error::new("bad serde"); + match SQLiteWasmDatabaseError::SerdeError(serde_err) { + SQLiteWasmDatabaseError::SerdeError(inner) => { + assert!(inner.to_string().contains("bad serde")); + } + _ => panic!("expected SerdeError variant"), + } + } +} diff --git a/packages/sqlite-web/src/params.rs b/packages/sqlite-web/src/params.rs index e703358..478dccb 100644 --- a/packages/sqlite-web/src/params.rs +++ b/packages/sqlite-web/src/params.rs @@ -90,3 +90,92 @@ fn encode_binary_to_obj(bytes: Vec) -> Result { + assert_eq!(js.as_string().as_deref(), Some("params must be an array")) + } + _ => panic!("expected JsError"), + } + } + + #[wasm_bindgen_test] + fn normalize_one_param_rejects_non_finite_numbers() { + assert!(normalize_one_param(&JsValue::from_f64(f64::NAN), 0).is_err()); + assert!(normalize_one_param(&JsValue::from_f64(f64::INFINITY), 0).is_err()); + } + + #[wasm_bindgen_test] + fn encode_bigint_marks_type() { + let encoded = encode_bigint_to_obj(BigInt::from(5u8)).expect("encodes bigint"); + let ty = Reflect::get(&encoded, &JsValue::from_str("__type")) + .unwrap() + .as_string(); + assert_eq!(ty.as_deref(), Some("bigint")); + let val = Reflect::get(&encoded, &JsValue::from_str("value")) + .unwrap() + .as_string(); + assert_eq!(val.as_deref(), Some("5")); + } + + #[wasm_bindgen_test] + fn encode_binary_emits_base64() { + let encoded = + encode_binary_to_obj(vec![1u8, 2, 3, 4]).expect("binary encoding should succeed"); + let ty = Reflect::get(&encoded, &JsValue::from_str("__type")) + .unwrap() + .as_string(); + assert_eq!(ty.as_deref(), Some("blob")); + + let base64_val = Reflect::get(&encoded, &JsValue::from_str("base64")) + .unwrap() + .as_string() + .unwrap(); + let expected = base64::engine::general_purpose::STANDARD.encode([1u8, 2, 3, 4]); + assert_eq!(base64_val, expected); + } + + #[wasm_bindgen_test] + fn normalize_params_js_handles_arrays() { + let arr = Array::new(); + arr.push(&JsValue::from_f64(1.0)); + arr.push(&JsValue::from_str("abc")); + let buf = ArrayBuffer::new(2); + Uint8Array::new(&buf).copy_from(&[9u8, 8]); + arr.push(&JsValue::from(buf)); + + let normalized = normalize_params_js(&JsValue::from(arr)).expect("valid params"); + assert_eq!(normalized.length(), 3); + assert_eq!(normalized.get(0).as_f64(), Some(1.0)); + assert_eq!(normalized.get(1).as_string().as_deref(), Some("abc")); + let blob = normalized.get(2); + let b64 = Reflect::get(&blob, &JsValue::from_str("base64")) + .unwrap() + .as_string() + .unwrap(); + let expected = base64::engine::general_purpose::STANDARD.encode([9u8, 8]); + assert_eq!(b64, expected); + } +} diff --git a/packages/sqlite-web/src/ready.rs b/packages/sqlite-web/src/ready.rs index 016a5aa..45d96cc 100644 --- a/packages/sqlite-web/src/ready.rs +++ b/packages/sqlite-web/src/ready.rs @@ -92,3 +92,48 @@ fn create_ready_promise( reject_clone.borrow_mut().replace(reject); }) } + +#[cfg(all(test, target_family = "wasm"))] +mod tests { + use super::*; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[wasm_bindgen_test(async)] + async fn ready_signal_resolves_when_marked_ready() { + let signal = ReadySignal::new(); + let promise = signal.wait_promise().expect("promise exists"); + signal.mark_ready(); + + assert!( + matches!(signal.current_state(), InitializationState::Ready), + "state should transition to Ready" + ); + + wasm_bindgen_futures::JsFuture::from(promise) + .await + .expect("promise resolves"); + assert!( + signal.wait_promise().is_err(), + "promise should be dropped after resolution" + ); + } + + #[wasm_bindgen_test(async)] + async fn ready_signal_rejects_when_marked_failed() { + let signal = ReadySignal::new(); + let promise = signal.wait_promise().expect("promise exists"); + signal.mark_failed("boom".into()); + + match signal.current_state() { + InitializationState::Failed(reason) => assert_eq!(reason, "boom"), + other => panic!("expected Failed state, got {other:?}"), + } + + let err = wasm_bindgen_futures::JsFuture::from(promise) + .await + .expect_err("promise should reject"); + assert_eq!(err.as_string().as_deref(), Some("boom")); + } +} diff --git a/packages/sqlite-web/src/utils.rs b/packages/sqlite-web/src/utils.rs index dcea7f7..9a81bba 100644 --- a/packages/sqlite-web/src/utils.rs +++ b/packages/sqlite-web/src/utils.rs @@ -12,3 +12,32 @@ pub(crate) fn describe_js_value(value: &JsValue) -> String { } format!("{value:?}") } + +#[cfg(all(test, target_family = "wasm"))] +mod tests { + use super::*; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[wasm_bindgen_test] + fn describe_handles_strings_and_numbers() { + assert_eq!( + describe_js_value(&JsValue::from_str("abc")), + String::from("abc") + ); + assert_eq!(describe_js_value(&JsValue::from_f64(42.0)), "42"); + assert_eq!(describe_js_value(&JsValue::from_f64(3.14)), "3.14"); + } + + #[wasm_bindgen_test] + fn describe_falls_back_to_debug_repr() { + let obj: JsValue = js_sys::Object::new().into(); + let described = describe_js_value(&obj); + assert_eq!( + described, + format!("{obj:?}"), + "objects should fall back to Rust debug formatting" + ); + } +} diff --git a/packages/sqlite-web/src/worker.rs b/packages/sqlite-web/src/worker.rs index 9f57af6..c9eb76e 100644 --- a/packages/sqlite-web/src/worker.rs +++ b/packages/sqlite-web/src/worker.rs @@ -112,3 +112,140 @@ fn handle_query_result_message( let _ = resolve.call1(&JsValue::NULL, &JsValue::from_str(&result_str)); } } + +#[cfg(all(test, target_family = "wasm"))] +mod tests { + use super::*; + use crate::ready::InitializationState; + use std::cell::RefCell; + use std::collections::HashMap; + use std::rc::Rc; + use wasm_bindgen::closure::Closure; + use wasm_bindgen::JsCast; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + fn recorder_function() -> (js_sys::Function, Rc>>) { + let calls = Rc::new(RefCell::new(Vec::new())); + let calls_clone = Rc::clone(&calls); + let closure = Closure::wrap(Box::new(move |value: JsValue| { + calls_clone + .borrow_mut() + .push(value.as_string().unwrap_or_else(|| format!("{value:?}"))); + }) as Box); + let func: js_sys::Function = closure.as_ref().unchecked_ref::().clone(); + closure.forget(); + (func, calls) + } + + #[wasm_bindgen_test] + fn worker_control_message_marks_ready() { + let signal = ReadySignal::new(); + let msg = js_sys::Object::new(); + let _ = js_sys::Reflect::set( + &msg, + &JsValue::from_str("type"), + &JsValue::from_str("worker-ready"), + ); + + let handled = handle_worker_control_message(&msg.into(), &signal); + assert!(handled); + assert!(matches!(signal.current_state(), InitializationState::Ready)); + } + + #[wasm_bindgen_test] + fn worker_control_message_marks_failed_with_reason() { + let signal = ReadySignal::new(); + let msg = js_sys::Object::new(); + let _ = js_sys::Reflect::set( + &msg, + &JsValue::from_str("type"), + &JsValue::from_str("worker-error"), + ); + let _ = js_sys::Reflect::set( + &msg, + &JsValue::from_str("error"), + &JsValue::from_str("boom"), + ); + + let handled = handle_worker_control_message(&msg.into(), &signal); + assert!(handled); + match signal.current_state() { + InitializationState::Failed(reason) => assert_eq!(reason, "boom"), + other => panic!("expected Failed state, got {other:?}"), + } + } + + #[wasm_bindgen_test] + fn query_result_message_resolves_registered_pending_call() { + let (resolve_fn, resolve_calls) = recorder_function(); + let (reject_fn, reject_calls) = recorder_function(); + let pending_queries = Rc::new(RefCell::new(HashMap::new())); + pending_queries + .borrow_mut() + .insert(7, (resolve_fn, reject_fn)); + + let msg = js_sys::Object::new(); + let _ = js_sys::Reflect::set( + &msg, + &JsValue::from_str("type"), + &JsValue::from_str("query-result"), + ); + let _ = js_sys::Reflect::set( + &msg, + &JsValue::from_str("requestId"), + &JsValue::from_f64(7.0), + ); + let _ = js_sys::Reflect::set(&msg, &JsValue::from_str("result"), &JsValue::from_str("ok")); + + let msg: JsValue = msg.into(); + handle_query_result_message(&msg, &pending_queries); + + { + let calls = resolve_calls.borrow(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0], "ok"); + } + assert!(reject_calls.borrow().is_empty()); + assert!(pending_queries.borrow().is_empty()); + } + + #[wasm_bindgen_test] + fn query_result_message_rejects_with_error_payload() { + let (resolve_fn, resolve_calls) = recorder_function(); + let (reject_fn, reject_calls) = recorder_function(); + let pending_queries = Rc::new(RefCell::new(HashMap::new())); + pending_queries + .borrow_mut() + .insert(3, (resolve_fn, reject_fn)); + + let msg = js_sys::Object::new(); + let _ = js_sys::Reflect::set( + &msg, + &JsValue::from_str("type"), + &JsValue::from_str("query-result"), + ); + let _ = js_sys::Reflect::set( + &msg, + &JsValue::from_str("requestId"), + &JsValue::from_f64(3.0), + ); + let _ = js_sys::Reflect::set( + &msg, + &JsValue::from_str("error"), + &JsValue::from_str("nope"), + ); + + let msg: JsValue = msg.into(); + handle_query_result_message(&msg, &pending_queries); + + assert!(resolve_calls.borrow().is_empty()); + { + let calls = reject_calls.borrow(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0], "nope"); + } + assert!(pending_queries.borrow().is_empty()); + } +} diff --git a/packages/sqlite-web/src/worker_template.rs b/packages/sqlite-web/src/worker_template.rs index 7268942..457fe93 100644 --- a/packages/sqlite-web/src/worker_template.rs +++ b/packages/sqlite-web/src/worker_template.rs @@ -12,3 +12,34 @@ pub fn generate_self_contained_worker(db_name: &str) -> String { let body = include_str!("embedded_worker.js"); format!("{}{}", prefix, body) } + +#[cfg(all(test, target_family = "wasm"))] +mod tests { + use super::*; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[wasm_bindgen_test] + fn embeds_db_name_and_timeout_configuration() { + let output = generate_self_contained_worker("my_db"); + assert!( + output.contains("self.__SQLITE_DB_NAME = \"my_db\";"), + "db name should be JSON encoded in prefix" + ); + assert!( + output.contains("self.__SQLITE_FOLLOWER_TIMEOUT_MS = 5000.0;"), + "timeout constant should be injected" + ); + } + + #[wasm_bindgen_test] + fn appends_embedded_worker_body() { + let output = generate_self_contained_worker("whatever"); + let body = include_str!("embedded_worker.js"); + assert!( + output.ends_with(body), + "template output should append embedded worker body verbatim" + ); + } +} From 9e4cfdd76498fbb069465a9695d60c1463f1d767 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 10 Nov 2025 16:08:31 +0100 Subject: [PATCH 12/15] AI comment updates --- packages/sqlite-web-core/src/coordination.rs | 23 ++++++++--- packages/sqlite-web-core/src/messages.rs | 22 +++++++++-- packages/sqlite-web-core/src/worker.rs | 23 ++++++++++- packages/sqlite-web/src/db.rs | 41 +++++++++++++++----- packages/sqlite-web/src/lib.rs | 1 + packages/sqlite-web/src/messages.rs | 3 ++ packages/sqlite-web/src/ready.rs | 14 +++++-- packages/sqlite-web/src/worker.rs | 41 +++++++++++++------- svelte-test/package-lock.json | 2 +- 9 files changed, 132 insertions(+), 38 deletions(-) create mode 100644 packages/sqlite-web/src/messages.rs diff --git a/packages/sqlite-web-core/src/coordination.rs b/packages/sqlite-web-core/src/coordination.rs index 2f20204..d779d53 100644 --- a/packages/sqlite-web-core/src/coordination.rs +++ b/packages/sqlite-web-core/src/coordination.rs @@ -9,7 +9,7 @@ use wasm_bindgen_futures::{spawn_local, JsFuture}; use web_sys::{BroadcastChannel, DedicatedWorkerGlobalScope}; use crate::database::SQLiteDatabase; -use crate::messages::{ChannelMessage, PendingQuery}; +use crate::messages::{ChannelMessage, PendingQuery, WORKER_ERROR_TYPE_INITIALIZATION_PENDING}; use crate::util::{js_value_to_string, sanitize_identifier, set_js_property}; // Worker state @@ -156,10 +156,16 @@ impl WorkerState { let has_leader = Rc::clone(&self.has_leader); let channel = self.channel.clone(); let worker_id = self.worker_id.clone(); + let follower_timeout_ms = self.follower_timeout_ms; spawn_local(async move { - const MAX_ATTEMPTS: u32 = 40; + const POLL_INTERVAL_MS: f64 = 250.0; + let max_attempts = if follower_timeout_ms.is_finite() && follower_timeout_ms > 0.0 { + (follower_timeout_ms / POLL_INTERVAL_MS).ceil() as u32 + } else { + 1 + }; let mut attempts = 0; - while attempts < MAX_ATTEMPTS { + while attempts < max_attempts { attempts += 1; if *has_leader.borrow() { break; @@ -171,7 +177,12 @@ impl WorkerState { let _ = send_worker_error_message(&err_msg); break; } - sleep_ms(250).await; + sleep_ms(POLL_INTERVAL_MS as i32).await; + } + if !*has_leader.borrow() { + let timeout = follower_timeout_ms.max(0.0); + let message = format!("Leader election timed out after {:.0}ms", timeout); + let _ = send_worker_error_message(&message); } }); } @@ -261,7 +272,7 @@ impl WorkerState { exec_on_db(Rc::clone(&self.db), sql, params).await } else { if !*self.has_leader.borrow() { - return Err("InitializationPending".to_string()); + return Err(WORKER_ERROR_TYPE_INITIALIZATION_PENDING.to_string()); } let query_id = Uuid::new_v4().to_string(); @@ -659,7 +670,7 @@ mod tests { .await; match result { Err(msg) => assert_eq!( - msg, "InitializationPending", + msg, WORKER_ERROR_TYPE_INITIALIZATION_PENDING, "Follower should reject while leader is pending" ), Ok(_) => panic!("Expected initialization error for follower"), diff --git a/packages/sqlite-web-core/src/messages.rs b/packages/sqlite-web-core/src/messages.rs index 8b1fd3d..b094468 100644 --- a/packages/sqlite-web-core/src/messages.rs +++ b/packages/sqlite-web-core/src/messages.rs @@ -1,6 +1,18 @@ use js_sys::Function; use serde::{Deserialize, Serialize}; +pub const WORKER_ERROR_TYPE_GENERIC: &str = "WorkerError"; +pub const WORKER_ERROR_TYPE_INITIALIZATION_PENDING: &str = "InitializationPending"; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct WorkerErrorPayload { + #[serde(rename = "type")] + pub error_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub message: Option, +} + // Message types for BroadcastChannel communication #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(tag = "type")] @@ -57,7 +69,7 @@ pub enum MainThreadMessage { #[serde(rename = "requestId")] request_id: u32, result: Option, - error: Option, + error: Option, }, #[serde(rename = "worker-ready")] WorkerReady, @@ -177,10 +189,14 @@ mod tests { let error_result = MainThreadMessage::QueryResult { request_id: 8, result: None, - error: Some("Database error".to_string()), + error: Some(WorkerErrorPayload { + error_type: WORKER_ERROR_TYPE_GENERIC.to_string(), + message: Some("Database error".to_string()), + }), }; assert_serialization_roundtrip(error_result, "query-result", |json| { - assert!(json.contains("\"error\":\"Database error\"")); + assert!(json.contains("\"type\":\"WorkerError\"")); + assert!(json.contains("\"message\":\"Database error\"")); assert!(json.contains("\"result\":null")); assert!(json.contains("\"requestId\":8")); }); diff --git a/packages/sqlite-web-core/src/worker.rs b/packages/sqlite-web-core/src/worker.rs index cbb3c35..8882712 100644 --- a/packages/sqlite-web-core/src/worker.rs +++ b/packages/sqlite-web-core/src/worker.rs @@ -8,7 +8,9 @@ use wasm_bindgen_futures::spawn_local; use web_sys::{DedicatedWorkerGlobalScope, MessageEvent}; use crate::coordination::WorkerState; -use crate::messages::WorkerMessage; +use crate::messages::{ + WorkerMessage, WORKER_ERROR_TYPE_GENERIC, WORKER_ERROR_TYPE_INITIALIZATION_PENDING, +}; use crate::util::{js_value_to_string, set_js_property}; // Global state @@ -45,6 +47,22 @@ fn post_message(obj: &js_sys::Object) -> Result<(), JsValue> { worker_scope.post_message(obj.as_ref()) } +fn make_structured_error(err: &str) -> Result { + let error_object = js_sys::Object::new(); + let error_type = if err == WORKER_ERROR_TYPE_INITIALIZATION_PENDING { + WORKER_ERROR_TYPE_INITIALIZATION_PENDING + } else { + WORKER_ERROR_TYPE_GENERIC + }; + set_js_property( + error_object.as_ref(), + "type", + &JsValue::from_str(error_type), + )?; + set_js_property(error_object.as_ref(), "message", &JsValue::from_str(err))?; + Ok(error_object.into()) +} + fn make_query_result_message( request_id: u32, result: Result, @@ -63,7 +81,8 @@ fn make_query_result_message( } Err(err) => { set_js_property(&response, "result", &JsValue::NULL)?; - set_js_property(&response, "error", &JsValue::from_str(&err))?; + let error_value = make_structured_error(&err)?; + set_js_property(&response, "error", &error_value)?; } } Ok(response) diff --git a/packages/sqlite-web/src/db.rs b/packages/sqlite-web/src/db.rs index 8349ce7..3f0150b 100644 --- a/packages/sqlite-web/src/db.rs +++ b/packages/sqlite-web/src/db.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use js_sys::Array; +use js_sys::{Array, Reflect}; use serde::Serialize; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; @@ -10,6 +10,7 @@ use wasm_bindgen_utils::prelude::*; use web_sys::Worker; use crate::errors::SQLiteWasmDatabaseError; +use crate::messages::WORKER_ERROR_TYPE_INITIALIZATION_PENDING; use crate::params::normalize_params_js; use crate::ready::{InitializationState, ReadySignal}; use crate::utils::describe_js_value; @@ -97,7 +98,6 @@ impl SQLiteWasmDatabase { }, Err(err) => { let reason = describe_js_value(&err); - self.ready_signal.mark_failed(reason.clone()); Err(SQLiteWasmDatabaseError::InitializationFailed(reason)) } } @@ -169,12 +169,7 @@ impl SQLiteWasmDatabase { let result = match JsFuture::from(promise).await { Ok(value) => value, Err(err) => { - if err - .as_string() - .as_deref() - .map(|s| s == "InitializationPending") - .unwrap_or(false) - { + if is_initialization_pending_error(&err) { return Err(SQLiteWasmDatabaseError::InitializationPending); } return Err(SQLiteWasmDatabaseError::JsError(err)); @@ -184,11 +179,21 @@ impl SQLiteWasmDatabase { } } +fn is_initialization_pending_error(err: &JsValue) -> bool { + let error_type = Reflect::get(err, &JsValue::from_str("type")) + .ok() + .and_then(|value| value.as_string()); + if error_type.as_deref() == Some(WORKER_ERROR_TYPE_INITIALIZATION_PENDING) { + return true; + } + err.as_string().as_deref() == Some(WORKER_ERROR_TYPE_INITIALIZATION_PENDING) +} + #[cfg(all(test, target_family = "wasm"))] mod tests { use super::*; use base64::Engine; - use js_sys::{Array, ArrayBuffer, BigInt, Uint8Array}; + use js_sys::{Array, ArrayBuffer, BigInt, Object, Uint8Array}; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -271,4 +276,22 @@ mod tests { other => panic!("expected JsError, got {other:?}"), } } + + #[wasm_bindgen_test] + fn detects_structured_initialization_pending_errors() { + let err = Object::new(); + let _ = js_sys::Reflect::set( + &err, + &JsValue::from_str("type"), + &JsValue::from_str(WORKER_ERROR_TYPE_INITIALIZATION_PENDING), + ); + let js_val: JsValue = err.into(); + assert!(is_initialization_pending_error(&js_val)); + } + + #[wasm_bindgen_test] + fn detects_string_initialization_pending_errors() { + let js_val = JsValue::from_str(WORKER_ERROR_TYPE_INITIALIZATION_PENDING); + assert!(is_initialization_pending_error(&js_val)); + } } diff --git a/packages/sqlite-web/src/lib.rs b/packages/sqlite-web/src/lib.rs index 35bd109..86a0b38 100644 --- a/packages/sqlite-web/src/lib.rs +++ b/packages/sqlite-web/src/lib.rs @@ -1,5 +1,6 @@ mod db; mod errors; +mod messages; mod params; mod ready; mod utils; diff --git a/packages/sqlite-web/src/messages.rs b/packages/sqlite-web/src/messages.rs new file mode 100644 index 0000000..d36b53c --- /dev/null +++ b/packages/sqlite-web/src/messages.rs @@ -0,0 +1,3 @@ +#[cfg(all(test, target_family = "wasm"))] +pub const WORKER_ERROR_TYPE_GENERIC: &str = "WorkerError"; +pub const WORKER_ERROR_TYPE_INITIALIZATION_PENDING: &str = "InitializationPending"; diff --git a/packages/sqlite-web/src/ready.rs b/packages/sqlite-web/src/ready.rs index 45d96cc..54a2bfa 100644 --- a/packages/sqlite-web/src/ready.rs +++ b/packages/sqlite-web/src/ready.rs @@ -52,12 +52,20 @@ impl ReadySignal { } pub(crate) fn mark_ready(&self) { + let mut transitioned = false; { let mut state = self.state.borrow_mut(); - if matches!(*state, InitializationState::Ready) { - return; + match *state { + InitializationState::Failed(_) => return, + InitializationState::Ready => {} + _ => { + *state = InitializationState::Ready; + transitioned = true; + } } - *state = InitializationState::Ready; + } + if !transitioned { + return; } if let Some(resolve) = self.resolve.borrow_mut().take() { let _ = resolve.call0(&JsValue::NULL); diff --git a/packages/sqlite-web/src/worker.rs b/packages/sqlite-web/src/worker.rs index c9eb76e..77c2f99 100644 --- a/packages/sqlite-web/src/worker.rs +++ b/packages/sqlite-web/src/worker.rs @@ -1,11 +1,10 @@ -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; - use crate::ready::ReadySignal; use crate::utils::describe_js_value; use js_sys::{Array, Function, Reflect}; use serde::Deserialize; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; @@ -99,8 +98,7 @@ fn handle_query_result_message( .filter(|e| !e.is_null() && !e.is_undefined()); if let Some(error) = error { - let error_str = error.as_string().unwrap_or_else(|| format!("{error:?}")); - let _ = reject.call1(&JsValue::NULL, &JsValue::from_str(&error_str)); + let _ = reject.call1(&JsValue::NULL, &error); return; } @@ -116,6 +114,7 @@ fn handle_query_result_message( #[cfg(all(test, target_family = "wasm"))] mod tests { use super::*; + use crate::messages::WORKER_ERROR_TYPE_GENERIC; use crate::ready::InitializationState; use std::cell::RefCell; use std::collections::HashMap; @@ -126,13 +125,11 @@ mod tests { wasm_bindgen_test_configure!(run_in_browser); - fn recorder_function() -> (js_sys::Function, Rc>>) { + fn recorder_function() -> (js_sys::Function, Rc>>) { let calls = Rc::new(RefCell::new(Vec::new())); let calls_clone = Rc::clone(&calls); let closure = Closure::wrap(Box::new(move |value: JsValue| { - calls_clone - .borrow_mut() - .push(value.as_string().unwrap_or_else(|| format!("{value:?}"))); + calls_clone.borrow_mut().push(value); }) as Box); let func: js_sys::Function = closure.as_ref().unchecked_ref::().clone(); closure.forget(); @@ -205,7 +202,7 @@ mod tests { { let calls = resolve_calls.borrow(); assert_eq!(calls.len(), 1); - assert_eq!(calls[0], "ok"); + assert_eq!(calls[0].as_string().as_deref(), Some("ok")); } assert!(reject_calls.borrow().is_empty()); assert!(pending_queries.borrow().is_empty()); @@ -231,11 +228,18 @@ mod tests { &JsValue::from_str("requestId"), &JsValue::from_f64(3.0), ); + let error_obj = js_sys::Object::new(); let _ = js_sys::Reflect::set( - &msg, - &JsValue::from_str("error"), + &error_obj, + &JsValue::from_str("type"), + &JsValue::from_str(WORKER_ERROR_TYPE_GENERIC), + ); + let _ = js_sys::Reflect::set( + &error_obj, + &JsValue::from_str("message"), &JsValue::from_str("nope"), ); + let _ = js_sys::Reflect::set(&msg, &JsValue::from_str("error"), error_obj.as_ref()); let msg: JsValue = msg.into(); handle_query_result_message(&msg, &pending_queries); @@ -244,7 +248,16 @@ mod tests { { let calls = reject_calls.borrow(); assert_eq!(calls.len(), 1); - assert_eq!(calls[0], "nope"); + let val = &calls[0]; + assert!(val.is_object()); + let error_type = js_sys::Reflect::get(val, &JsValue::from_str("type")) + .unwrap() + .as_string(); + assert_eq!(error_type.as_deref(), Some(WORKER_ERROR_TYPE_GENERIC)); + let message = js_sys::Reflect::get(val, &JsValue::from_str("message")) + .unwrap() + .as_string(); + assert_eq!(message.as_deref(), Some("nope")); } assert!(pending_queries.borrow().is_empty()); } diff --git a/svelte-test/package-lock.json b/svelte-test/package-lock.json index 85ada02..b945f38 100644 --- a/svelte-test/package-lock.json +++ b/svelte-test/package-lock.json @@ -914,7 +914,7 @@ "node_modules/@rainlanguage/sqlite-web": { "version": "0.0.1-alpha.7", "resolved": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.7.tgz", - "integrity": "sha512-wq6GlwbKYEn8pY8m63mcenYOOesJLCfsduF7JojWj7prIg5dN+gZw0zfam8rI1N1elzpm3nbfvgYGJRpsdkJqg==" + "integrity": "sha512-+oe41z7h/k2fMUS8B74gtmaLhyAjDgmLvGWpjHi1V6JJ1AW830JN3lyyPUUof48fzeSmzZPvGNyUjHDHbq3XCQ==" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.53.2", From 73bd2c383e6e1c20651060a78b40e135cc4292e3 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 10 Nov 2025 16:53:12 +0100 Subject: [PATCH 13/15] ai comment update --- packages/sqlite-web-core/src/coordination.rs | 39 ++++++++++++++++---- svelte-test/package-lock.json | 2 +- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/sqlite-web-core/src/coordination.rs b/packages/sqlite-web-core/src/coordination.rs index d779d53..8358a7d 100644 --- a/packages/sqlite-web-core/src/coordination.rs +++ b/packages/sqlite-web-core/src/coordination.rs @@ -159,14 +159,24 @@ impl WorkerState { let follower_timeout_ms = self.follower_timeout_ms; spawn_local(async move { const POLL_INTERVAL_MS: f64 = 250.0; - let max_attempts = if follower_timeout_ms.is_finite() && follower_timeout_ms > 0.0 { - (follower_timeout_ms / POLL_INTERVAL_MS).ceil() as u32 + let mut remaining_ms = if follower_timeout_ms.is_finite() { + follower_timeout_ms.max(0.0) } else { - 1 + f64::INFINITY }; - let mut attempts = 0; - while attempts < max_attempts { - attempts += 1; + + if remaining_ms <= 0.0 { + if !*has_leader.borrow() { + let message = format!( + "Leader election timed out after {:.0}ms", + follower_timeout_ms.max(0.0) + ); + let _ = send_worker_error_message(&message); + } + return; + } + + while remaining_ms.is_infinite() || remaining_ms > 0.0 { if *has_leader.borrow() { break; } @@ -177,7 +187,22 @@ impl WorkerState { let _ = send_worker_error_message(&err_msg); break; } - sleep_ms(POLL_INTERVAL_MS as i32).await; + if *has_leader.borrow() { + break; + } + + let sleep_duration = if remaining_ms.is_infinite() { + POLL_INTERVAL_MS + } else { + remaining_ms.min(POLL_INTERVAL_MS) + }; + if sleep_duration <= 0.0 { + break; + } + sleep_ms(sleep_duration.ceil() as i32).await; + if remaining_ms.is_finite() { + remaining_ms -= sleep_duration; + } } if !*has_leader.borrow() { let timeout = follower_timeout_ms.max(0.0); diff --git a/svelte-test/package-lock.json b/svelte-test/package-lock.json index b945f38..790637d 100644 --- a/svelte-test/package-lock.json +++ b/svelte-test/package-lock.json @@ -914,7 +914,7 @@ "node_modules/@rainlanguage/sqlite-web": { "version": "0.0.1-alpha.7", "resolved": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.7.tgz", - "integrity": "sha512-+oe41z7h/k2fMUS8B74gtmaLhyAjDgmLvGWpjHi1V6JJ1AW830JN3lyyPUUof48fzeSmzZPvGNyUjHDHbq3XCQ==" + "integrity": "sha512-dW24eFg/V00W85vp3y5Ezf7hFwH4k4FNcpmL7ME2MUVYPat2vw31if/kPNLwqAvDV1Bc5m5toicwgsMNOpC3IQ==" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.53.2", From 578cd488cd448d1767b06c3c68460f3ecc57f631 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Wed, 12 Nov 2025 14:29:11 +0100 Subject: [PATCH 14/15] review refactor --- packages/sqlite-web-core/src/coordination.rs | 124 ++++++++++++------- packages/sqlite-web-core/src/worker.rs | 17 +-- svelte-test/package-lock.json | 20 +-- 3 files changed, 101 insertions(+), 60 deletions(-) diff --git a/packages/sqlite-web-core/src/coordination.rs b/packages/sqlite-web-core/src/coordination.rs index 8358a7d..9f342bb 100644 --- a/packages/sqlite-web-core/src/coordination.rs +++ b/packages/sqlite-web-core/src/coordination.rs @@ -12,11 +12,23 @@ use crate::database::SQLiteDatabase; use crate::messages::{ChannelMessage, PendingQuery, WORKER_ERROR_TYPE_INITIALIZATION_PENDING}; use crate::util::{js_value_to_string, sanitize_identifier, set_js_property}; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LeadershipRole { + Leader, + Follower, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LeaderPresence { + Known, + Unknown, +} + // Worker state pub struct WorkerState { pub worker_id: String, - pub is_leader: Rc>, - pub has_leader: Rc>, + pub is_leader: Rc>, + pub has_leader: Rc>, pub db: Rc>>, pub channel: BroadcastChannel, pub db_name: String, @@ -110,8 +122,8 @@ impl WorkerState { Ok(WorkerState { worker_id, - is_leader: Rc::new(RefCell::new(false)), - has_leader: Rc::new(RefCell::new(false)), + is_leader: Rc::new(RefCell::new(LeadershipRole::Follower)), + has_leader: Rc::new(RefCell::new(LeaderPresence::Unknown)), db: Rc::new(RefCell::new(None)), channel, db_name: db_name_raw, @@ -150,7 +162,7 @@ impl WorkerState { } pub fn start_leader_probe(self: &Rc) { - if *self.is_leader.borrow() { + if matches!(*self.is_leader.borrow(), LeadershipRole::Leader) { return; } let has_leader = Rc::clone(&self.has_leader); @@ -166,7 +178,7 @@ impl WorkerState { }; if remaining_ms <= 0.0 { - if !*has_leader.borrow() { + if matches!(*has_leader.borrow(), LeaderPresence::Unknown) { let message = format!( "Leader election timed out after {:.0}ms", follower_timeout_ms.max(0.0) @@ -177,7 +189,7 @@ impl WorkerState { } while remaining_ms.is_infinite() || remaining_ms > 0.0 { - if *has_leader.borrow() { + if matches!(*has_leader.borrow(), LeaderPresence::Known) { break; } let ping = ChannelMessage::LeaderPing { @@ -187,7 +199,7 @@ impl WorkerState { let _ = send_worker_error_message(&err_msg); break; } - if *has_leader.borrow() { + if matches!(*has_leader.borrow(), LeaderPresence::Known) { break; } @@ -204,7 +216,7 @@ impl WorkerState { remaining_ms -= sleep_duration; } } - if !*has_leader.borrow() { + if matches!(*has_leader.borrow(), LeaderPresence::Unknown) { let timeout = follower_timeout_ms.max(0.0); let message = format!("Leader election timed out after {:.0}ms", timeout); let _ = send_worker_error_message(&message); @@ -229,8 +241,8 @@ impl WorkerState { set_js_property(&options, "mode", &JsValue::from_str("exclusive"))?; let handler = Closure::once(move |_lock: JsValue| -> Promise { - *is_leader.borrow_mut() = true; - *has_leader.borrow_mut() = true; + *is_leader.borrow_mut() = LeadershipRole::Leader; + *has_leader.borrow_mut() = LeaderPresence::Known; let db = Rc::clone(&db); let channel = channel.clone(); @@ -242,7 +254,7 @@ impl WorkerState { match SQLiteDatabase::initialize_opfs(&db_name).await { Ok(database) => { *db.borrow_mut() = Some(database); - *has_leader_inner.borrow_mut() = true; + *has_leader_inner.borrow_mut() = LeaderPresence::Known; let msg = ChannelMessage::NewLeader { leader_id: worker_id.clone(), @@ -261,7 +273,7 @@ impl WorkerState { } Err(err) => { let msg = js_value_to_string(&err); - *has_leader_inner.borrow_mut() = false; + *has_leader_inner.borrow_mut() = LeaderPresence::Unknown; let _ = send_worker_error_message(&msg); } } @@ -293,10 +305,10 @@ impl WorkerState { sql: String, params: Option>, ) -> Result { - if *self.is_leader.borrow() { + if matches!(*self.is_leader.borrow(), LeadershipRole::Leader) { exec_on_db(Rc::clone(&self.db), sql, params).await } else { - if !*self.has_leader.borrow() { + if matches!(*self.has_leader.borrow(), LeaderPresence::Unknown) { return Err(WORKER_ERROR_TYPE_INITIALIZATION_PENDING.to_string()); } let query_id = Uuid::new_v4().to_string(); @@ -331,8 +343,8 @@ impl WorkerState { } fn handle_channel_message( - is_leader: &Rc>, - has_leader: &Rc>, + is_leader: &Rc>, + has_leader: &Rc>, db: &Rc>>, channel: &BroadcastChannel, pending_queries: &Rc>>, @@ -344,8 +356,8 @@ fn handle_channel_message( query_id, sql, params, - } => { - if *is_leader.borrow() { + } => match *is_leader.borrow() { + LeadershipRole::Leader => { let db = Rc::clone(db); let channel = channel.clone(); spawn_local(async move { @@ -361,7 +373,8 @@ fn handle_channel_message( } }); } - } + LeadershipRole::Follower => {} + }, ChannelMessage::QueryResponse { query_id, result, @@ -369,18 +382,21 @@ fn handle_channel_message( } => handle_query_response(pending_queries, query_id, result, error), ChannelMessage::NewLeader { leader_id: _ } => { let mut has_leader_ref = has_leader.borrow_mut(); - let already_had_leader = *has_leader_ref; - *has_leader_ref = true; + let previous_presence = *has_leader_ref; + *has_leader_ref = LeaderPresence::Known; drop(has_leader_ref); - if !already_had_leader { - if let Err(err_msg) = send_worker_ready_message() { - let _ = send_worker_error_message(&err_msg); + match previous_presence { + LeaderPresence::Unknown => { + if let Err(err_msg) = send_worker_ready_message() { + let _ = send_worker_error_message(&err_msg); + } } + LeaderPresence::Known => {} } } - ChannelMessage::LeaderPing { requester_id: _ } => { - if *is_leader.borrow() { + ChannelMessage::LeaderPing { requester_id: _ } => match *is_leader.borrow() { + LeadershipRole::Leader => { let response = ChannelMessage::NewLeader { leader_id: worker_id.to_string(), }; @@ -388,7 +404,8 @@ fn handle_channel_message( let _ = send_worker_error_message(&err_msg); } } - } + LeadershipRole::Follower => {} + }, } } @@ -553,8 +570,9 @@ mod tests { state.worker_id.contains('-'), "Worker ID should be valid UUID format" ); - assert!( - !*state.is_leader.borrow(), + assert_eq!( + *state.is_leader.borrow(), + LeadershipRole::Follower, "New workers should not start as leader" ); assert!( @@ -588,13 +606,25 @@ mod tests { #[wasm_bindgen_test] fn test_leadership_state_management() { if let Ok(state) = WorkerState::new() { - assert!(!*state.is_leader.borrow(), "Should start as follower"); + assert_eq!( + *state.is_leader.borrow(), + LeadershipRole::Follower, + "Should start as follower" + ); - *state.is_leader.borrow_mut() = true; - assert!(*state.is_leader.borrow(), "Should become leader"); + *state.is_leader.borrow_mut() = LeadershipRole::Leader; + assert_eq!( + *state.is_leader.borrow(), + LeadershipRole::Leader, + "Should become leader" + ); - *state.is_leader.borrow_mut() = false; - assert!(!*state.is_leader.borrow(), "Should become follower again"); + *state.is_leader.borrow_mut() = LeadershipRole::Follower; + assert_eq!( + *state.is_leader.borrow(), + LeadershipRole::Follower, + "Should become follower again" + ); } } @@ -658,7 +688,7 @@ mod tests { #[wasm_bindgen_test] async fn test_execute_query_leader_vs_follower_paths() { if let Ok(leader_state) = WorkerState::new() { - *leader_state.is_leader.borrow_mut() = true; + *leader_state.is_leader.borrow_mut() = LeadershipRole::Leader; let test_queries = vec![ "", @@ -685,8 +715,9 @@ mod tests { } if let Ok(follower_state) = WorkerState::new() { - assert!( - !*follower_state.is_leader.borrow(), + assert_eq!( + *follower_state.is_leader.borrow(), + LeadershipRole::Follower, "Should start as follower" ); @@ -716,7 +747,11 @@ mod tests { #[wasm_bindgen_test] async fn test_attempt_leadership_behavior() { if let Ok(state) = WorkerState::new() { - assert!(!*state.is_leader.borrow(), "Should start as follower"); + assert_eq!( + *state.is_leader.borrow(), + LeadershipRole::Follower, + "Should start as follower" + ); assert!( state.db.borrow().is_none(), "Database should be uninitialized" @@ -728,7 +763,11 @@ mod tests { let workers: Vec<_> = (0..3).filter_map(|_| WorkerState::new().ok()).collect(); if workers.len() >= 2 { for worker in &workers { - assert!(!*worker.is_leader.borrow(), "All should start as followers"); + assert_eq!( + *worker.is_leader.borrow(), + LeadershipRole::Follower, + "All should start as followers" + ); } for worker in &workers { @@ -749,9 +788,10 @@ mod tests { pending_clone.borrow().len() ); - *state.is_leader.borrow_mut() = true; - assert!( + *state.is_leader.borrow_mut() = LeadershipRole::Leader; + assert_eq!( *is_leader_clone.borrow(), + LeadershipRole::Leader, "Changes should be visible through cloned Rc" ); diff --git a/packages/sqlite-web-core/src/worker.rs b/packages/sqlite-web-core/src/worker.rs index 8882712..4e53461 100644 --- a/packages/sqlite-web-core/src/worker.rs +++ b/packages/sqlite-web-core/src/worker.rs @@ -186,6 +186,7 @@ pub fn main() -> Result<(), JsValue> { #[cfg(all(test, target_family = "wasm"))] mod tests { use super::*; + use crate::coordination::LeadershipRole; use js_sys::{Object, Reflect}; use std::rc::Rc; use wasm_bindgen_test::*; @@ -330,7 +331,7 @@ mod tests { if let Ok(state) = WorkerState::new() { let state_rc = Rc::new(state); - assert!(!*state_rc.is_leader.borrow()); + assert_eq!(*state_rc.is_leader.borrow(), LeadershipRole::Follower); assert!(state_rc.db.borrow().is_none()); assert!(state_rc.pending_queries.borrow().is_empty()); } @@ -341,10 +342,10 @@ mod tests { if let Ok(state) = WorkerState::new() { let state_rc = Rc::new(state); - assert!(!*state_rc.is_leader.borrow()); + assert_eq!(*state_rc.is_leader.borrow(), LeadershipRole::Follower); - *state_rc.is_leader.borrow_mut() = true; - assert!(*state_rc.is_leader.borrow()); + *state_rc.is_leader.borrow_mut() = LeadershipRole::Leader; + assert_eq!(*state_rc.is_leader.borrow(), LeadershipRole::Leader); } } @@ -363,8 +364,8 @@ mod tests { if let Ok(state) = WorkerState::new() { let state_rc = Rc::new(state); - *state_rc.is_leader.borrow_mut() = true; - assert!(*state_rc.is_leader.borrow()); + *state_rc.is_leader.borrow_mut() = LeadershipRole::Leader; + assert_eq!(*state_rc.is_leader.borrow(), LeadershipRole::Leader); assert!(state_rc.db.borrow().is_none()); } } @@ -411,8 +412,8 @@ mod tests { let leader_rc = Rc::new(leader_state); let follower_rc = Rc::new(follower_state); - *leader_rc.is_leader.borrow_mut() = true; - assert!(!*follower_rc.is_leader.borrow()); + *leader_rc.is_leader.borrow_mut() = LeadershipRole::Leader; + assert_eq!(*follower_rc.is_leader.borrow(), LeadershipRole::Follower); assert!(leader_rc.setup_channel_listener().is_ok()); assert!(follower_rc.setup_channel_listener().is_ok()); diff --git a/svelte-test/package-lock.json b/svelte-test/package-lock.json index 790637d..e5643f4 100644 --- a/svelte-test/package-lock.json +++ b/svelte-test/package-lock.json @@ -914,7 +914,7 @@ "node_modules/@rainlanguage/sqlite-web": { "version": "0.0.1-alpha.7", "resolved": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.7.tgz", - "integrity": "sha512-dW24eFg/V00W85vp3y5Ezf7hFwH4k4FNcpmL7ME2MUVYPat2vw31if/kPNLwqAvDV1Bc5m5toicwgsMNOpC3IQ==" + "integrity": "sha512-/0sRyRe38o21dCmxxNnjpmkcJPHD0KOJUzt82rmL8o0xkPYHygGJZ6t8ZuA/lZwezLlCaoXREu7hNZDEYCDKvw==" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.53.2", @@ -1403,9 +1403,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", - "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4726,9 +4726,9 @@ } }, "node_modules/svelte": { - "version": "5.43.5", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.5.tgz", - "integrity": "sha512-HQoZArIewxQVNedseDsgMgnRSC4XOXczxXLF9rOJaPIJkg58INOPUiL8aEtzqZIXNSZJyw8NmqObwg/voajiHQ==", + "version": "5.43.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.6.tgz", + "integrity": "sha512-RnyO9VXI85Bmsf4b8AuQFBKFYL3LKUl+ZrifOjvlrQoboAROj5IITVLK1yOXBjwUWUn2BI5cfmurktgCzuZ5QA==", "dev": true, "license": "MIT", "dependencies": { @@ -4752,9 +4752,9 @@ } }, "node_modules/svelte-check": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.3.tgz", - "integrity": "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.4.tgz", + "integrity": "sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==", "dev": true, "license": "MIT", "dependencies": { From 345d18f5e5e336ff4919f51c085a5e3e5466873a Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Wed, 12 Nov 2025 14:33:04 +0100 Subject: [PATCH 15/15] review update --- packages/sqlite-web/src/db.rs | 6 +++--- svelte-test/package-lock.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/sqlite-web/src/db.rs b/packages/sqlite-web/src/db.rs index 3f0150b..01f8505 100644 --- a/packages/sqlite-web/src/db.rs +++ b/packages/sqlite-web/src/db.rs @@ -168,10 +168,10 @@ impl SQLiteWasmDatabase { let result = match JsFuture::from(promise).await { Ok(value) => value, + Err(err) if is_initialization_pending_error(&err) => { + return Err(SQLiteWasmDatabaseError::InitializationPending); + } Err(err) => { - if is_initialization_pending_error(&err) { - return Err(SQLiteWasmDatabaseError::InitializationPending); - } return Err(SQLiteWasmDatabaseError::JsError(err)); } }; diff --git a/svelte-test/package-lock.json b/svelte-test/package-lock.json index e5643f4..13fbac8 100644 --- a/svelte-test/package-lock.json +++ b/svelte-test/package-lock.json @@ -914,7 +914,7 @@ "node_modules/@rainlanguage/sqlite-web": { "version": "0.0.1-alpha.7", "resolved": "file:../pkg/rainlanguage-sqlite-web-0.0.1-alpha.7.tgz", - "integrity": "sha512-/0sRyRe38o21dCmxxNnjpmkcJPHD0KOJUzt82rmL8o0xkPYHygGJZ6t8ZuA/lZwezLlCaoXREu7hNZDEYCDKvw==" + "integrity": "sha512-yCPVkRqqmpxuYrTRu4ch4BffBM8RhTMqNuyRH9/v8kKT+UXgooTKjPwzpbzIM9+5fSe54B4uOEayUOI1O1qxww==" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.53.2",