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", diff --git a/packages/sqlite-web-core/src/coordination.rs b/packages/sqlite-web-core/src/coordination.rs index 10fd273..9f342bb 100644 --- a/packages/sqlite-web-core/src/coordination.rs +++ b/packages/sqlite-web-core/src/coordination.rs @@ -5,21 +5,35 @@ use std::rc::Rc; use uuid::Uuid; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -use wasm_bindgen_futures::spawn_local; -use web_sys::BroadcastChannel; +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}; +#[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 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 +54,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,31 +102,56 @@ 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)), + 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, 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(); + let worker_id = self.worker_id.clone(); 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, + &worker_id, + msg, + ); } }) as Box); @@ -96,9 +161,73 @@ impl WorkerState { Ok(()) } + pub fn start_leader_probe(self: &Rc) { + if matches!(*self.is_leader.borrow(), LeadershipRole::Leader) { + return; + } + 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 POLL_INTERVAL_MS: f64 = 250.0; + let mut remaining_ms = if follower_timeout_ms.is_finite() { + follower_timeout_ms.max(0.0) + } else { + f64::INFINITY + }; + + if remaining_ms <= 0.0 { + if matches!(*has_leader.borrow(), LeaderPresence::Unknown) { + 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 matches!(*has_leader.borrow(), LeaderPresence::Known) { + 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; + } + if matches!(*has_leader.borrow(), LeaderPresence::Known) { + 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 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); + } + }); + } + 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(); @@ -112,17 +241,20 @@ impl WorkerState { set_js_property(&options, "mode", &JsValue::from_str("exclusive"))?; let handler = Closure::once(move |_lock: JsValue| -> Promise { - *is_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(); 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() = LeaderPresence::Known; let msg = ChannelMessage::NewLeader { leader_id: worker_id.clone(), @@ -135,8 +267,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() = LeaderPresence::Unknown; + let _ = send_worker_error_message(&msg); } - Err(_e) => {} } }); @@ -166,9 +305,12 @@ 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 matches!(*self.has_leader.borrow(), LeaderPresence::Unknown) { + return Err(WORKER_ERROR_TYPE_INITIALIZATION_PENDING.to_string()); + } let query_id = Uuid::new_v4().to_string(); let promise = Promise::new(&mut |resolve, reject| { @@ -182,7 +324,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( @@ -194,17 +336,19 @@ 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)), } } } } fn handle_channel_message( - is_leader: &Rc>, + is_leader: &Rc>, + has_leader: &Rc>, db: &Rc>>, channel: &BroadcastChannel, pending_queries: &Rc>>, + worker_id: &str, msg: ChannelMessage, ) { match msg { @@ -212,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 { @@ -229,13 +373,39 @@ fn handle_channel_message( } }); } - } + LeadershipRole::Follower => {} + }, ChannelMessage::QueryResponse { query_id, result, error, } => handle_query_response(pending_queries, query_id, result, error), - ChannelMessage::NewLeader { leader_id: _ } => {} + ChannelMessage::NewLeader { leader_id: _ } => { + let mut has_leader_ref = has_leader.borrow_mut(); + let previous_presence = *has_leader_ref; + *has_leader_ref = LeaderPresence::Known; + drop(has_leader_ref); + + 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: _ } => match *is_leader.borrow() { + LeadershipRole::Leader => { + 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); + } + } + LeadershipRole::Follower => {} + }, } } @@ -273,6 +443,46 @@ fn handle_query_response( } } +async fn sleep_ms(ms: i32) { + let promise = js_sys::Promise::new(&mut |resolve, _| { + let resolve_for_timeout = resolve.clone(); + let closure = Closure::once(move || { + let _ = resolve_for_timeout.call0(&JsValue::NULL); + }); + + 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; +} + async fn exec_on_db( db: Rc>>, sql: String, @@ -360,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!( @@ -395,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" + ); } } @@ -465,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![ "", @@ -492,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" ); @@ -501,12 +725,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, WORKER_ERROR_TYPE_INITIALIZATION_PENDING, + "Follower should reject while leader is pending" ), - Ok(_) => panic!("Expected timeout error for follower"), + Ok(_) => panic!("Expected initialization error for follower"), } } } @@ -524,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" @@ -536,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 { @@ -557,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" ); @@ -577,4 +809,9 @@ mod tests { ); } } + + #[wasm_bindgen_test(async)] + async fn test_sleep_ms_completes() { + sleep_ms(0).await; + } } diff --git a/packages/sqlite-web-core/src/messages.rs b/packages/sqlite-web-core/src/messages.rs index f72495d..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")] @@ -26,6 +38,11 @@ pub enum ChannelMessage { result: Option, error: Option, }, + #[serde(rename = "leader-ping")] + LeaderPing { + #[serde(rename = "requesterId")] + requester_id: String, + }, } // Messages from main thread @@ -52,7 +69,7 @@ pub enum MainThreadMessage { #[serde(rename = "requestId")] request_id: u32, result: Option, - error: Option, + error: Option, }, #[serde(rename = "worker-ready")] WorkerReady, @@ -124,6 +141,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] @@ -165,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 8fbf042..4e53461 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) @@ -127,9 +146,13 @@ 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().inspect_err(|err| { + let _ = send_worker_error(err.clone()); + })?); - state.setup_channel_listener()?; + state.setup_channel_listener().inspect_err(|err| { + let _ = send_worker_error(err.clone()); + })?; let state_clone = Rc::clone(&state); spawn_local(async move { @@ -143,6 +166,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(); @@ -162,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::*; @@ -306,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()); } @@ -317,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); } } @@ -339,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()); } } @@ -387,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/packages/sqlite-web/src/db.rs b/packages/sqlite-web/src/db.rs new file mode 100644 index 0000000..01f8505 --- /dev/null +++ b/packages/sqlite-web/src/db.rs @@ -0,0 +1,297 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + +use js_sys::{Array, Reflect}; +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::messages::WORKER_ERROR_TYPE_INITIALIZATION_PENDING; +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); + 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 is_initialization_pending_error(&err) => { + return Err(SQLiteWasmDatabaseError::InitializationPending); + } + Err(err) => { + return Err(SQLiteWasmDatabaseError::JsError(err)); + } + }; + Ok(result.as_string().unwrap_or_else(|| format!("{result:?}"))) + } +} + +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, Object, 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:?}"), + } + } + + #[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/errors.rs b/packages/sqlite-web/src/errors.rs new file mode 100644 index 0000000..f0a924d --- /dev/null +++ b/packages/sqlite-web/src/errors.rs @@ -0,0 +1,88 @@ +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(), + } + } +} + +#[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/lib.rs b/packages/sqlite-web/src/lib.rs index 13074e7..86a0b38 100644 --- a/packages/sqlite-web/src/lib.rs +++ b/packages/sqlite-web/src/lib.rs @@ -1,685 +1,14 @@ -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 messages; +mod params; +mod ready; +mod utils; +mod worker; mod worker_template; -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>>, -) { - let pending_queries_clone = Rc::clone(&pending_queries); - let onmessage = Closure::wrap(Box::new(move |event: MessageEvent| { - let data = event.data(); - if handle_worker_control_message(&data) { - 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) -> 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" { - return true; - } else if msg_type == "worker-error" { - let _ = js_sys::Reflect::get(data, &JsValue::from_str("error")); - return true; - } - } - } - false -} - -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), - ))) -} - -#[wasm_bindgen] -pub struct SQLiteWasmDatabase { - worker: Worker, - pending_queries: Rc>>, - next_request_id: Rc>, -} - -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() - } -} -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::new(trimmed).map_err(|e| { - serde::de::Error::custom(format!("Failed to create SQLiteWasmDatabase: {e:?}")) - }) - } -} - -#[derive(Debug, Error)] -pub enum SQLiteWasmDatabaseError { - #[error(transparent)] - SerdeError(#[from] serde_wasm_bindgen::Error), - #[error("JavaScript error: {0:?}")] - JsError(JsValue), -} - -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 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 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 next_request_id = Rc::new(RefCell::new(1u32)); - - Ok(SQLiteWasmDatabase { - worker, - pending_queries, - next_request_id, - }) - } - - 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) - }) - } - - /// 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)?; - - // 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 = JsFuture::from(promise).await?; - 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); - - #[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] - fn test_sqlite_wasm_database_serialization() { - let db = SQLiteWasmDatabase::new("testdb").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] - 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"); - - 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") { - 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 - ); - } - - // --- 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").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").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").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/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/params.rs b/packages/sqlite-web/src/params.rs new file mode 100644 index 0000000..478dccb --- /dev/null +++ b/packages/sqlite-web/src/params.rs @@ -0,0 +1,181 @@ +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()) +} + +#[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 ensure_array_accepts_null_like_values() { + let result = ensure_array(&JsValue::NULL).expect("null coerces to empty array"); + assert_eq!(result.length(), 0); + + let undef = ensure_array(&JsValue::UNDEFINED).expect("undefined coerces to empty array"); + assert_eq!(undef.length(), 0); + } + + #[wasm_bindgen_test] + fn ensure_array_rejects_non_arrays() { + let not_array = JsValue::from_str("nope"); + let err = ensure_array(¬_array).expect_err("strings should be rejected"); + match err { + SQLiteWasmDatabaseError::JsError(js) => { + 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 new file mode 100644 index 0000000..54a2bfa --- /dev/null +++ b/packages/sqlite-web/src/ready.rs @@ -0,0 +1,147 @@ +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 transitioned = false; + { + let mut state = self.state.borrow_mut(); + match *state { + InitializationState::Failed(_) => return, + InitializationState::Ready => {} + _ => { + *state = InitializationState::Ready; + transitioned = true; + } + } + } + if !transitioned { + return; + } + 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); + }) +} + +#[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/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..9a81bba --- /dev/null +++ b/packages/sqlite-web/src/utils.rs @@ -0,0 +1,43 @@ +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:?}") +} + +#[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 new file mode 100644 index 0000000..77c2f99 --- /dev/null +++ b/packages/sqlite-web/src/worker.rs @@ -0,0 +1,264 @@ +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; +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 _ = reject.call1(&JsValue::NULL, &error); + 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)); + } +} + +#[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; + 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 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].as_string().as_deref(), Some("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 error_obj = js_sys::Object::new(); + let _ = js_sys::Reflect::set( + &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); + + assert!(resolve_calls.borrow().is_empty()); + { + let calls = reject_calls.borrow(); + assert_eq!(calls.len(), 1); + 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/packages/sqlite-web/src/worker_template.rs b/packages/sqlite-web/src/worker_template.rs index 4209abf..457fe93 100644 --- a/packages/sqlite-web/src/worker_template.rs +++ b/packages/sqlite-web/src/worker_template.rs @@ -4,8 +4,42 @@ 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) } + +#[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" + ); + } +} 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/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 7b1f650..13fbac8 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-yCPVkRqqmpxuYrTRu4ch4BffBM8RhTMqNuyRH9/v8kKT+UXgooTKjPwzpbzIM9+5fSe54B4uOEayUOI1O1qxww==" }, "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.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "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.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": { @@ -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" } } 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!; } /**