Add SQLite3MultipleCiphers support

This commit is contained in:
Spxg
2025-05-29 22:44:39 +08:00
parent 81561cb51d
commit 09d499d5a3
16 changed files with 264 additions and 147 deletions

View File

@@ -29,6 +29,12 @@ jobs:
- name: Build - name: Build
run: | run: |
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
cd ..
trunk build --release trunk build --release
- name: Deploy - name: Deploy

30
Cargo.lock generated
View File

@@ -170,6 +170,15 @@ version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
[[package]]
name = "cc"
version = "1.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
dependencies = [
"shlex",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@@ -1773,6 +1782,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@@ -1851,9 +1866,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlite-wasm-rs" name = "sqlite-wasm-rs"
version = "0.3.6" version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/Spxg/sqlite-wasm-rs#9912b461e8e551feb400b01f3abd94e4303bef21"
checksum = "c0c6fdb636283dad1283b20bc62157ac68a7124fde269723467689c4fd6d356c"
dependencies = [ dependencies = [
"cc",
"fragile", "fragile",
"indexed_db_futures", "indexed_db_futures",
"js-sys", "js-sys",
@@ -1861,6 +1876,7 @@ dependencies = [
"parking_lot", "parking_lot",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"wasm-array-cp",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
@@ -2168,6 +2184,16 @@ dependencies = [
"wit-bindgen-rt", "wit-bindgen-rt",
] ]
[[package]]
name = "wasm-array-cp"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb633b3e235f0ebe0a35162adc1e0293fc4b7e3f3a6fc7b5374d80464267ff84"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.100" version = "0.2.100"

View File

@@ -18,14 +18,14 @@ aceditor = { path = "crates/aceditor" }
split-grid = { path = "crates/split-grid" } split-grid = { path = "crates/split-grid" }
floating-ui = { path = "crates/floating-ui" } floating-ui = { path = "crates/floating-ui" }
sqlite-wasm-rs = { version = "0.3.6", default-features = false, features = ["precompiled"] } sqlite-wasm-rs = { git = "https://github.com/Spxg/sqlite-wasm-rs", default-features = false, optional = true }
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
leptos = { version = "0.8.2", features = ["csr"] } leptos = { version = "0.8.2", features = ["csr"] }
reactive_stores = "0.2.2" reactive_stores = "0.2.2"
thiserror = "2.0.12" thiserror = "2.0.12"
serde = "1.0.219" serde = "1.0.219"
serde_json = "1.0.140" serde_json = "1.0.140"
web-sys = { version = "0.3.77", features = ["BlobPropertyBag", "Clipboard", "DedicatedWorkerGlobalScope", "File", "FileList", "HtmlSelectElement", "MediaQueryList", "Navigator", "Storage", "Worker", "WorkerOptions", "WorkerType"] } web-sys = { version = "0.3.77", features = ["BlobPropertyBag", "Clipboard", "DedicatedWorkerGlobalScope", "DomException", "File", "FileList", "HtmlSelectElement", "MediaQueryList", "Navigator", "Storage", "Worker", "WorkerOptions", "WorkerType"] }
serde-wasm-bindgen = "0.6.5" serde-wasm-bindgen = "0.6.5"
parking_lot = "0.12.3" parking_lot = "0.12.3"
once_cell = "1.21.3" once_cell = "1.21.3"
@@ -39,3 +39,8 @@ fragile = "2.0.1"
hex = "0.4.3" hex = "0.4.3"
prettytable-rs = "0.10.0" prettytable-rs = "0.10.0"
sqlformat = "0.3.5" sqlformat = "0.3.5"
[features]
default = ["sqlite3"]
sqlite3 = ["sqlite-wasm-rs/precompiled"]
sqlite3mc = ["sqlite-wasm-rs/sqlite3mc"]

View File

@@ -53,8 +53,9 @@
<link data-trunk href="./assets/module.postcss/output/section.module.css" rel="css"> <link data-trunk href="./assets/module.postcss/output/section.module.css" rel="css">
<link data-trunk href="./assets/module.postcss/output/share.module.css" rel="css"> <link data-trunk href="./assets/module.postcss/output/share.module.css" rel="css">
<link data-trunk rel="rust" href="Cargo.toml" data-bin="app" data-type="main" /> <link data-trunk rel="rust" href="Cargo.toml" data-cargo-no-default-features data-bin="app" data-type="main" />
<link data-trunk rel="rust" href="Cargo.toml" data-bin="worker" data-type="worker" data-loader-shim data-bindgen-target="web" /> <link data-trunk rel="rust" href="Cargo.toml" data-cargo-no-default-features data-cargo-features="sqlite3" data-bin="sqlite3" data-type="worker" data-loader-shim data-bindgen-target="web" />
<link data-trunk rel="rust" href="Cargo.toml" data-cargo-no-default-features data-cargo-features="sqlite3mc" data-bin="sqlite3mc" data-type="worker" data-loader-shim data-bindgen-target="web" />
</head> </head>
<body></body> <body></body>
</html> </html>

View File

@@ -2,7 +2,8 @@ use leptos::prelude::*;
use reactive_stores::Store; use reactive_stores::Store;
use crate::app::{ use crate::app::{
GlobalState, GlobalStateStoreFields, config_element::Either, menu_group::MenuGroup, GlobalState, GlobalStateStoreFields, config_element::Either, menu_aside::MenuAside,
menu_group::MenuGroup,
}; };
#[component] #[component]
@@ -25,6 +26,29 @@ pub fn AdvancedOptionsMenu() -> impl IntoView {
state.run_selected_sql().set(*value); state.run_selected_sql().set(*value);
} }
/> />
<Either
aside=Some(
view! {
<MenuAside>
"Using an encrypted database.
https://github.com/utelle/SQLite3MultipleCiphers
https://utelle.github.io/SQLite3MultipleCiphers"
</MenuAside>
}
.into_any(),
)
id="multiple_ciphers".into()
name="Multiple Ciphers".into()
a=true
b=false
a_label=Some("On".to_string())
b_label=Some("Off".to_string())
value=move || *state.multiple_ciphers().read()
is_default=Box::new(move || !*state.multiple_ciphers().read())
on_change=move |value: &bool| {
state.multiple_ciphers().set(*value);
}
/>
</MenuGroup> </MenuGroup>
</> </>
} }

View File

@@ -13,6 +13,7 @@ istyles!(
#[component] #[component]
pub fn Either<E, V, T>( pub fn Either<E, V, T>(
#[prop(default =None)] aside: Option<AnyView>,
on_change: E, on_change: E,
id: String, id: String,
a: T, a: T,
@@ -58,7 +59,7 @@ where
let b2 = Arc::clone(&b1); let b2 = Arc::clone(&b1);
view! { view! {
<ConfigElement name=name is_default=is_default> <ConfigElement name=name is_default=is_default aside>
<div class=styles::toggle> <div class=styles::toggle>
<input <input
id=a_id.clone() id=a_id.clone()
@@ -105,6 +106,7 @@ where
#[component] #[component]
pub fn ConfigElement( pub fn ConfigElement(
name: String, name: String,
#[prop(default =None)] aside: Option<AnyView>,
#[prop(default = Box::new(|| true))] is_default: Box<dyn Fn() -> bool + Send>, #[prop(default = Box::new(|| true))] is_default: Box<dyn Fn() -> bool + Send>,
children: Children, children: Children,
) -> impl IntoView { ) -> impl IntoView {
@@ -121,6 +123,7 @@ pub fn ConfigElement(
<span class=style>{name}</span> <span class=style>{name}</span>
<div class=styles::value>{children()}</div> <div class=styles::value>{children()}</div>
</div> </div>
{aside}
</MenuItem> </MenuItem>
} }
} }

View File

@@ -21,6 +21,7 @@ use crate::{
tools_menu::ToolsMenu, tools_menu::ToolsMenu,
vfs_menu::VfsMenu, vfs_menu::VfsMenu,
}, },
send_request,
}; };
istyles!(styles, "assets/module.postcss/header.module.css.map"); istyles!(styles, "assets/module.postcss/header.module.css.map");
@@ -90,8 +91,9 @@ pub fn execute(state: Store<GlobalState>) -> Box<dyn Fn() + Send + 'static> {
let run_selected_code = let run_selected_code =
!selected_code.is_empty() && state.run_selected_sql().get_untracked(); !selected_code.is_empty() && state.run_selected_sql().get_untracked();
if let Some(worker) = &*state.worker().read_untracked() { send_request(
worker.send_task(WorkerRequest::Run(RunOptions { state,
WorkerRequest::Run(RunOptions {
embed: false, embed: false,
sql: if run_selected_code { sql: if run_selected_code {
selected_code selected_code
@@ -99,8 +101,8 @@ pub fn execute(state: Store<GlobalState>) -> Box<dyn Fn() + Send + 'static> {
code code
}, },
clear_on_prepare: !*state.keep_ctx().read_untracked(), clear_on_prepare: !*state.keep_ctx().read_untracked(),
})); }),
} );
}) })
} }
@@ -248,13 +250,14 @@ fn ToolsButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
let sql = editor.get_value(); let sql = editor.get_value();
drop(editor_guard); drop(editor_guard);
if let Some(worker) = &*state.worker().read_untracked() { send_request(
worker.send_task(WorkerRequest::Run(RunOptions { state,
WorkerRequest::Run(RunOptions {
embed: true, embed: true,
sql, sql,
clear_on_prepare: !*state.keep_ctx().read_untracked(), clear_on_prepare: !*state.keep_ctx().read_untracked(),
})); }),
} );
signal.set(false); signal.set(false);
}; };
@@ -339,9 +342,7 @@ fn DatabaseButton(
let result = reader.result().unwrap(); let result = reader.result().unwrap();
let array_buffer = result.unchecked_into::<js_sys::ArrayBuffer>(); let array_buffer = result.unchecked_into::<js_sys::ArrayBuffer>();
let data = js_sys::Uint8Array::new(&array_buffer); let data = js_sys::Uint8Array::new(&array_buffer);
if let Some(worker) = &*state.worker().read_untracked() { send_request(state, WorkerRequest::LoadDb(LoadDbOptions { data }));
worker.send_task(WorkerRequest::LoadDb(LoadDbOptions { data }));
}
}) })
as Box<dyn FnMut(_)>)); as Box<dyn FnMut(_)>));
@@ -415,9 +416,7 @@ fn DatabaseButton(
}); });
let on_download = move |_: MouseEvent, signal: WriteSignal<bool>| { let on_download = move |_: MouseEvent, signal: WriteSignal<bool>| {
if let Some(worker) = &*state.worker().read() { send_request(state, WorkerRequest::DownloadDb);
worker.send_task(WorkerRequest::DownloadDb);
}
signal.set(false); signal.set(false);
}; };

View File

@@ -21,5 +21,5 @@ mod state;
mod tools_menu; mod tools_menu;
mod vfs_menu; mod vfs_menu;
pub use playground::playground; pub use playground::Playground;
pub use state::*; pub use state::*;

View File

@@ -6,9 +6,7 @@ use leptos::tachys::html;
use prettytable::{Cell, Row, Table}; use prettytable::{Cell, Row, Table};
use reactive_stores::Store; use reactive_stores::Store;
use split_grid::{Gutter, SplitOptions}; use split_grid::{Gutter, SplitOptions};
use tokio::sync::mpsc::UnboundedReceiver;
use wasm_bindgen::{JsCast, prelude::Closure}; use wasm_bindgen::{JsCast, prelude::Closure};
use wasm_bindgen_futures::spawn_local;
use web_sys::wasm_bindgen::JsValue; use web_sys::wasm_bindgen::JsValue;
use crate::{ use crate::{
@@ -18,43 +16,34 @@ use crate::{
editor::Editor, editor::Editor,
header::Header, header::Header,
output::{Output, change_focus}, output::{Output, change_focus},
state::{GlobalState, GlobalStateStoreFields, Orientation, Theme, Vfs}, state::{GlobalState, GlobalStateStoreFields, Orientation, Theme},
}, },
}; };
use crate::{WorkerHandle, WorkerResponse, handle_state};
istyles!(styles, "assets/module.postcss/playground.module.css.map"); istyles!(styles, "assets/module.postcss/playground.module.css.map");
pub fn playground( #[component]
worker: (WorkerHandle, UnboundedReceiver<WorkerResponse>), pub fn Playground() -> impl IntoView {
) -> Box<dyn FnOnce() -> AnyView + 'static> { let state = GlobalState::load().unwrap_or_default();
Box::new(move || { provide_context(Store::new(state));
let (worker_handle, rx) = worker;
let state = GlobalState::load().unwrap_or_default();
provide_context(Store::new(state));
let state = expect_context::<Store<GlobalState>>(); let state = expect_context::<Store<GlobalState>>();
state.worker().set(Some(worker_handle));
handle_last_error(state); handle_last_error(state);
handle_system_theme(state); handle_system_theme(state);
handle_automic_orientation(state); handle_automic_orientation(state);
handle_connect_db(state); handle_save_state(state);
handle_save_state(state); handle_import_progress(state);
handle_import_progress(state); handle_ace_config(state);
handle_ace_config(state); handle_embed_query_result(state);
handle_embed_query_result(state);
spawn_local(handle_state(state, rx)); view! {
<div id="playground" class=styles::container>
view! { <Header />
<div id="playground" class=styles::container> <ResizableArea />
<Header /> </div>
<ResizableArea /> }
</div> .into_any()
}
.into_any()
})
} }
fn handle_embed_query_result(state: Store<GlobalState>) { fn handle_embed_query_result(state: Store<GlobalState>) {
@@ -154,22 +143,12 @@ fn handle_save_state(state: Store<GlobalState>) {
state.keep_ctx().track(); state.keep_ctx().track();
state.sql().track(); state.sql().track();
state.run_selected_sql().track(); state.run_selected_sql().track();
state.multiple_ciphers().track();
state.read_untracked().save(); state.read_untracked().save();
}); });
} }
fn handle_connect_db(state: Store<GlobalState>) {
Effect::new(move || {
if let Some(worker) = &*state.worker().read() {
worker.send_task(crate::WorkerRequest::Open(crate::OpenOptions {
filename: "test.db".into(),
persist: *state.vfs().read() == Vfs::OPFS,
}));
}
});
}
fn handle_last_error(state: Store<GlobalState>) { fn handle_last_error(state: Store<GlobalState>) {
Effect::new(move || { Effect::new(move || {
if state.last_error().read().is_some() { if state.last_error().read().is_some() {

View File

@@ -7,7 +7,7 @@ use reactive_stores::Store;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use web_sys::MediaQueryList; use web_sys::MediaQueryList;
use crate::{FragileComfirmed, SQLightError, SQLiteStatementResult, WorkerHandle}; use crate::{FragileComfirmed, SQLightError, SQLiteStatementResult};
const DEFAULT_CODE: &str = "PRAGMA page_size=4096; const DEFAULT_CODE: &str = "PRAGMA page_size=4096;
@@ -34,10 +34,9 @@ pub struct GlobalState {
keep_ctx: bool, keep_ctx: bool,
sql: String, sql: String,
run_selected_sql: bool, run_selected_sql: bool,
multiple_ciphers: bool,
// runtime state below // runtime state below
#[serde(skip)] #[serde(skip)]
worker: Option<WorkerHandle>,
#[serde(skip)]
editor: Option<Editor>, editor: Option<Editor>,
#[serde(skip)] #[serde(skip)]
focus: Option<Focus>, focus: Option<Focus>,
@@ -71,7 +70,7 @@ impl Default for GlobalState {
keep_ctx: false, keep_ctx: false,
sql: DEFAULT_CODE.into(), sql: DEFAULT_CODE.into(),
run_selected_sql: false, run_selected_sql: false,
worker: None, multiple_ciphers: false,
editor: None, editor: None,
focus: None, focus: None,
is_focused: false, is_focused: false,

View File

@@ -1,5 +1,5 @@
use leptos::prelude::*; use leptos::prelude::*;
use sqlight::{app::playground, setup_worker}; use sqlight::app::Playground;
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
@@ -7,5 +7,5 @@ use wasm_bindgen::prelude::wasm_bindgen;
async fn main() { async fn main() {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
console_log::init_with_level(log::Level::Debug).unwrap(); console_log::init_with_level(log::Level::Debug).unwrap();
mount_to_body(playground(setup_worker().await)); mount_to_body(Playground);
} }

3
src/bin/sqlite3.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
sqlight::worker::entry();
}

3
src/bin/sqlite3mc.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
sqlight::worker::entry();
}

View File

@@ -1,42 +0,0 @@
use sqlight::{WorkerRequest, WorkerResponse, worker};
use tokio::sync::mpsc::UnboundedReceiver;
use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
use wasm_bindgen_futures::spawn_local;
use web_sys::{DedicatedWorkerGlobalScope, MessageEvent};
fn main() {
console_error_panic_hook::set_once();
console_log::init_with_level(log::Level::Debug).unwrap();
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<JsValue>();
let scope: DedicatedWorkerGlobalScope = JsValue::from(js_sys::global()).into();
spawn_local(execute_task(scope.clone(), rx));
let on_message = Closure::<dyn Fn(MessageEvent)>::new(move |ev: MessageEvent| {
tx.send(ev.data()).unwrap();
});
scope.set_onmessage(Some(on_message.as_ref().unchecked_ref()));
scope
.post_message(&serde_wasm_bindgen::to_value(&WorkerResponse::Ready).unwrap())
.expect("Faild to send ready to window");
on_message.forget();
}
async fn execute_task(scope: DedicatedWorkerGlobalScope, mut rx: UnboundedReceiver<JsValue>) {
while let Some(request) = rx.recv().await {
let request = serde_wasm_bindgen::from_value::<WorkerRequest>(request).unwrap();
let resp = match request {
WorkerRequest::Open(options) => WorkerResponse::Open(worker::open(options).await),
WorkerRequest::Run(options) => WorkerResponse::Run(worker::run(options).await),
WorkerRequest::LoadDb(options) => {
WorkerResponse::LoadDb(worker::load_db(options).await)
}
WorkerRequest::DownloadDb => WorkerResponse::DownloadDb(worker::download_db().await),
};
if let Err(err) = scope.post_message(&serde_wasm_bindgen::to_value(&resp).unwrap()) {
log::error!("Failed to send task to window: {resp:?}, {err:?}");
}
}
}

View File

@@ -1,18 +1,20 @@
pub mod app; pub mod app;
#[cfg(any(feature = "sqlite3", feature = "sqlite3mc"))]
pub mod worker; pub mod worker;
use aceditor::EditorError; use aceditor::EditorError;
use app::{Exported, GlobalState, GlobalStateStoreFields}; use app::{Exported, GlobalState, GlobalStateStoreFields, Vfs};
use fragile::Fragile; use fragile::Fragile;
use js_sys::Uint8Array; use js_sys::Uint8Array;
use leptos::prelude::*; use leptos::prelude::*;
use reactive_stores::Store; use reactive_stores::Store;
use std::{ use std::{
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
sync::Arc, sync::{Arc, Once},
}; };
use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::{OnceCell, mpsc::UnboundedReceiver};
use wasm_bindgen::{JsCast, prelude::Closure}; use wasm_bindgen::{JsCast, prelude::Closure};
use wasm_bindgen_futures::spawn_local;
use web_sys::{MessageEvent, Worker, WorkerOptions, WorkerType}; use web_sys::{MessageEvent, Worker, WorkerOptions, WorkerType};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -51,8 +53,6 @@ impl<T> DerefMut for FragileComfirmed<T> {
} }
} }
pub const PERSIST_VFS: &str = "sqlight-sahpool";
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum SQLightError { pub enum SQLightError {
#[error(transparent)] #[error(transparent)]
@@ -127,16 +127,6 @@ pub struct LoadDbOptions {
pub data: Uint8Array, pub data: Uint8Array,
} }
impl OpenOptions {
pub fn uri(&self) -> String {
format!(
"file:{}?vfs={}",
self.filename,
if self.persist { PERSIST_VFS } else { "memvfs" }
)
}
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct RunOptions { pub struct RunOptions {
pub sql: String, pub sql: String,
@@ -209,9 +199,55 @@ impl WorkerHandle {
unsafe impl Send for WorkerHandle {} unsafe impl Send for WorkerHandle {}
unsafe impl Sync for WorkerHandle {} unsafe impl Sync for WorkerHandle {}
pub async fn setup_worker() -> (WorkerHandle, UnboundedReceiver<WorkerResponse>) { fn send_request(state: Store<GlobalState>, req: WorkerRequest) {
let uri = "./worker_loader.js"; spawn_local(async move {
if state.multiple_ciphers().get_untracked() {
sqlite3mc(state).await
} else {
sqlite3(state).await
}
.send_task(req);
});
}
async fn sqlite3mc(state: Store<GlobalState>) -> &'static WorkerHandle {
static ONCE: Once = Once::new();
static WORKER: OnceCell<WorkerHandle> = OnceCell::const_new();
let worker = WORKER
.get_or_init(|| async { setup_worker(state, "./sqlite3mc_loader.js").await })
.await;
ONCE.call_once(|| {
connect_db(state, worker);
Effect::new(move || connect_db(state, worker));
});
worker
}
async fn sqlite3(state: Store<GlobalState>) -> &'static WorkerHandle {
static ONCE: Once = Once::new();
static WORKER: OnceCell<WorkerHandle> = OnceCell::const_new();
let worker = WORKER
.get_or_init(|| async { setup_worker(state, "./sqlite3_loader.js").await })
.await;
ONCE.call_once(|| {
connect_db(state, worker);
Effect::new(move || connect_db(state, worker));
});
worker
}
fn connect_db(state: Store<GlobalState>, handle: &'static WorkerHandle) {
handle.send_task(crate::WorkerRequest::Open(crate::OpenOptions {
filename: "test.db".into(),
persist: *state.vfs().read() == Vfs::OPFS,
}));
}
async fn setup_worker(state: Store<GlobalState>, uri: &str) -> WorkerHandle {
let opts = WorkerOptions::new(); let opts = WorkerOptions::new();
opts.set_type(WorkerType::Module); opts.set_type(WorkerType::Module);
@@ -233,14 +269,16 @@ pub async fn setup_worker() -> (WorkerHandle, UnboundedReceiver<WorkerResponse>)
} }
}); });
spawn_local(handle_state(state, rx));
worker.set_onmessage(Some(on_message.as_ref().unchecked_ref())); worker.set_onmessage(Some(on_message.as_ref().unchecked_ref()));
on_message.forget(); on_message.forget();
wait.notified().await; wait.notified().await;
(WorkerHandle(worker), rx) WorkerHandle(worker)
} }
pub async fn handle_state(state: Store<GlobalState>, mut rx: UnboundedReceiver<WorkerResponse>) { async fn handle_state(state: Store<GlobalState>, mut rx: UnboundedReceiver<WorkerResponse>) {
while let Some(resp) = rx.recv().await { while let Some(resp) = rx.recv().await {
state.last_error().set(None); state.last_error().set(None);

View File

@@ -1,8 +1,8 @@
mod sqlitend; mod sqlitend;
use crate::{ use crate::{
DownloadDbResponse, LoadDbOptions, OpenOptions, PERSIST_VFS, RunOptions, SQLiteRunResult, DownloadDbResponse, LoadDbOptions, OpenOptions, RunOptions, SQLiteRunResult, WorkerError,
WorkerError, WorkerRequest, WorkerResponse,
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use sqlite_wasm_rs::{ use sqlite_wasm_rs::{
@@ -14,6 +14,10 @@ use sqlitend::SQLiteDb;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
use tokio::sync::mpsc::UnboundedReceiver;
use wasm_bindgen::{JsCast, JsValue, prelude::Closure};
use wasm_bindgen_futures::spawn_local;
use web_sys::{DedicatedWorkerGlobalScope, MessageEvent};
type Result<T> = std::result::Result<T, WorkerError>; type Result<T> = std::result::Result<T, WorkerError>;
@@ -24,6 +28,29 @@ static FS_UTIL: Lazy<FSUtil> = Lazy::new(|| FSUtil {
opfs: OnceCell::new(), opfs: OnceCell::new(),
}); });
#[cfg(feature = "sqlite3")]
const OPFS_VFS: &str = "opfs";
#[cfg(feature = "sqlite3mc")]
const OPFS_VFS: &str = "multipleciphers-opfs";
#[cfg(feature = "sqlite3")]
const OPFS_VFS_DIR: &str = "sqlight-sahpool";
#[cfg(feature = "sqlite3mc")]
const OPFS_VFS_DIR: &str = "sqlight-sahpool-mc";
#[cfg(feature = "sqlite3")]
const MEM_VFS: &str = "memvfs";
#[cfg(feature = "sqlite3mc")]
const MEM_VFS: &str = "multipleciphers-memvfs";
fn uri(filename: &str, persist: bool) -> String {
format!(
"file:{}?vfs={}",
filename,
if persist { OPFS_VFS } else { MEM_VFS }
)
}
struct FSUtil { struct FSUtil {
mem: MemVfsUtil, mem: MemVfsUtil,
opfs: OnceCell<OpfsSAHPoolUtil>, opfs: OnceCell<OpfsSAHPoolUtil>,
@@ -53,8 +80,8 @@ async fn init_opfs_util() -> Result<&'static OpfsSAHPoolUtil> {
sqlite_wasm_rs::sahpool_vfs::install( sqlite_wasm_rs::sahpool_vfs::install(
Some( Some(
&OpfsSAHPoolCfgBuilder::new() &OpfsSAHPoolCfgBuilder::new()
.directory(PERSIST_VFS) .directory(OPFS_VFS_DIR)
.vfs_name(PERSIST_VFS) .vfs_name("opfs")
.build(), .build(),
), ),
false, false,
@@ -69,7 +96,7 @@ fn get_opfs_util() -> Result<&'static OpfsSAHPoolUtil> {
FS_UTIL.opfs.get().ok_or(WorkerError::Unexpected) FS_UTIL.opfs.get().ok_or(WorkerError::Unexpected)
} }
pub async fn download_db() -> Result<DownloadDbResponse> { async fn download_db() -> Result<DownloadDbResponse> {
with_worker(|worker| { with_worker(|worker| {
let filename = &worker.open_options.filename; let filename = &worker.open_options.filename;
let db = if worker.open_options.persist { let db = if worker.open_options.persist {
@@ -90,34 +117,42 @@ pub async fn download_db() -> Result<DownloadDbResponse> {
.await .await
} }
pub async fn load_db(options: LoadDbOptions) -> Result<()> { async fn load_db(options: LoadDbOptions) -> Result<()> {
let db = copy_to_vec(&options.data); let db = copy_to_vec(&options.data);
#[cfg(feature = "sqlite3")]
sqlite_wasm_rs::utils::check_import_db(&db)
.map_err(|err| WorkerError::LoadDb(format!("{err}")))?;
with_worker(|worker| { with_worker(|worker| {
let _ = std::mem::replace(&mut worker.state, SQLiteState::NotOpened); drop(std::mem::replace(&mut worker.state, SQLiteState::NotOpened));
let filename = &worker.open_options.filename; let filename = &worker.open_options.filename;
if worker.open_options.persist { if worker.open_options.persist {
let opfs = get_opfs_util()?; let opfs = get_opfs_util()?;
opfs.unlink(filename).map_err(|_| WorkerError::Unexpected)?; opfs.unlink(filename).map_err(|_| WorkerError::Unexpected)?;
if let Err(err) = opfs.import_db(filename, &db) {
if let Err(err) = opfs.import_db_unchecked(filename, &db) {
return Err(WorkerError::LoadDb(format!("{err}"))); return Err(WorkerError::LoadDb(format!("{err}")));
} }
} else { } else {
let mem_vfs = &FS_UTIL.mem; let mem_vfs = &FS_UTIL.mem;
mem_vfs.delete_db(filename); mem_vfs.delete_db(filename);
if let Err(err) = mem_vfs.import_db(filename, &db) { if let Err(err) = mem_vfs.import_db_unchecked(filename, &db, 114514 /* unused */) {
return Err(WorkerError::LoadDb(format!("{err}"))); return Err(WorkerError::LoadDb(format!("{err}")));
} }
} }
worker.state = SQLiteState::Opened(SQLiteDb::open(&worker.open_options.uri())?); worker.state = SQLiteState::Opened(SQLiteDb::open(&uri(
&worker.open_options.filename,
worker.open_options.persist,
))?);
Ok(()) Ok(())
}) })
.await .await
} }
pub async fn open(options: OpenOptions) -> Result<()> { async fn open(options: OpenOptions) -> Result<()> {
let mut locker = DB.lock().await; let mut locker = DB.lock().await;
locker.take(); locker.take();
@@ -125,7 +160,7 @@ pub async fn open(options: OpenOptions) -> Result<()> {
init_opfs_util().await?; init_opfs_util().await?;
} }
let state = SQLiteState::Opened(SQLiteDb::open(&options.uri())?); let state = SQLiteState::Opened(SQLiteDb::open(&uri(&options.filename, options.persist))?);
let worker = SQLiteWorker { let worker = SQLiteWorker {
open_options: options, open_options: options,
state, state,
@@ -134,10 +169,10 @@ pub async fn open(options: OpenOptions) -> Result<()> {
Ok(()) Ok(())
} }
pub async fn run(options: RunOptions) -> Result<SQLiteRunResult> { async fn run(options: RunOptions) -> Result<SQLiteRunResult> {
with_worker(|worker| { with_worker(|worker| {
if options.clear_on_prepare { if options.clear_on_prepare {
let _ = std::mem::replace(&mut worker.state, SQLiteState::NotOpened); drop(std::mem::replace(&mut worker.state, SQLiteState::NotOpened));
let filename = &worker.open_options.filename; let filename = &worker.open_options.filename;
if worker.open_options.persist { if worker.open_options.persist {
@@ -149,7 +184,10 @@ pub async fn run(options: RunOptions) -> Result<SQLiteRunResult> {
mem_vfs.delete_db(filename); mem_vfs.delete_db(filename);
} }
worker.state = SQLiteState::Opened(SQLiteDb::open(&worker.open_options.uri())?); worker.state = SQLiteState::Opened(SQLiteDb::open(&uri(
&worker.open_options.filename,
worker.open_options.persist,
))?);
} }
match &worker.state { match &worker.state {
SQLiteState::NotOpened => Err(WorkerError::InvaildState), SQLiteState::NotOpened => Err(WorkerError::InvaildState),
@@ -165,3 +203,38 @@ pub async fn run(options: RunOptions) -> Result<SQLiteRunResult> {
}) })
.await .await
} }
async fn execute_task(scope: DedicatedWorkerGlobalScope, mut rx: UnboundedReceiver<JsValue>) {
while let Some(request) = rx.recv().await {
let request = serde_wasm_bindgen::from_value::<WorkerRequest>(request).unwrap();
let resp = match request {
WorkerRequest::Open(options) => WorkerResponse::Open(open(options).await),
WorkerRequest::Run(options) => WorkerResponse::Run(run(options).await),
WorkerRequest::LoadDb(options) => WorkerResponse::LoadDb(load_db(options).await),
WorkerRequest::DownloadDb => WorkerResponse::DownloadDb(download_db().await),
};
if let Err(err) = scope.post_message(&serde_wasm_bindgen::to_value(&resp).unwrap()) {
log::error!("Failed to send task to window: {resp:?}, {err:?}");
}
}
}
pub fn entry() {
console_error_panic_hook::set_once();
console_log::init_with_level(log::Level::Debug).unwrap();
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<JsValue>();
let scope: DedicatedWorkerGlobalScope = JsValue::from(js_sys::global()).into();
spawn_local(execute_task(scope.clone(), rx));
let on_message = Closure::<dyn Fn(MessageEvent)>::new(move |ev: MessageEvent| {
tx.send(ev.data()).unwrap();
});
scope.set_onmessage(Some(on_message.as_ref().unchecked_ref()));
scope
.post_message(&serde_wasm_bindgen::to_value(&WorkerResponse::Ready).unwrap())
.expect("Faild to send ready to window");
on_message.forget();
}