Add import and load db support

This commit is contained in:
Spxg
2025-05-18 14:30:24 +08:00
parent e84d5af619
commit 4aa23c5596
8 changed files with 247 additions and 33 deletions

View File

@@ -1,11 +1,13 @@
use istyles::istyles;
use leptos::{prelude::*, tachys::html};
use leptos::{html::Input, prelude::*, tachys::html};
use reactive_stores::Store;
use web_sys::{Url, UrlSearchParams};
use wasm_bindgen::{JsCast, prelude::Closure};
use web_sys::{Event, FileReader, HtmlInputElement, Url, UrlSearchParams};
use crate::{
PrepareOptions, WorkerRequest,
FragileComfirmed, LoadDbOptions, PrepareOptions, SQLightError, WorkerRequest,
app::{
ImportProgress, Vfs,
advanced_options_menu::AdvancedOptionsMenu,
button_set::{Button, ButtonSet, IconButton, Rule},
config_menu::ConfigMenu,
@@ -24,6 +26,8 @@ istyles!(styles, "assets/module.postcss/header.module.css.map");
pub fn Header() -> impl IntoView {
let menu_container = NodeRef::new();
let input_ref = NodeRef::new();
view! {
<>
<div id="header" class=styles::container>
@@ -41,6 +45,10 @@ pub fn Header() -> impl IntoView {
</ButtonSet>
</div>
<div class=styles::right>
<input type="file" node_ref=input_ref style="display: none" />
<ButtonSet>
<LoadButton input_ref=input_ref />
</ButtonSet>
<ButtonSet>
<ShareButton />
</ButtonSet>
@@ -99,6 +107,108 @@ fn ExecuteButton() -> impl IntoView {
}
}
#[component]
fn LoadButton(input_ref: NodeRef<Input>) -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();
let (file, set_file) = signal::<Option<FragileComfirmed<web_sys::File>>>(None);
Effect::new(move || {
if let Some(file) = &*file.read() {
let filename = file.name();
if let Ok(reader) = FileReader::new() {
let on_progress = FragileComfirmed::new(Closure::wrap(Box::new(
move |ev: web_sys::ProgressEvent| {
if ev.length_computable() {
state.import_progress().set(Some(ImportProgress {
filename: filename.clone(),
loaded: ev.loaded(),
total: ev.total(),
}));
}
},
)
as Box<dyn FnMut(_)>));
let on_error = FragileComfirmed::new(Closure::wrap(Box::new(
move |ev: web_sys::ProgressEvent| {
if let Some(target) = ev.target() {
let reader = target.unchecked_into::<FileReader>();
if let Some(dom_error) = reader.error() {
state.last_error().set(Some(FragileComfirmed::new(
SQLightError::ImportDb(dom_error.message().to_string()),
)));
}
}
},
)
as Box<dyn FnMut(_)>));
let on_load =
FragileComfirmed::new(Closure::wrap(Box::new(move |ev: web_sys::Event| {
if let Some(target) = ev.target() {
let reader = target.unchecked_into::<FileReader>();
if let Ok(result) = reader.result() {
let array_buffer = result.unchecked_into::<js_sys::ArrayBuffer>();
let data = js_sys::Uint8Array::new(&array_buffer);
let persist = state.vfs().get() != Vfs::Memory;
if let Some(worker) = &*state.worker().read() {
worker.send_task(WorkerRequest::LoadDb(LoadDbOptions {
// FIXME: multi db
id: String::new(),
persist,
data,
}));
}
}
}
})
as Box<dyn FnMut(_)>));
reader.set_onprogress(Some(on_progress.as_ref().unchecked_ref()));
reader.set_onerror(Some(on_error.as_ref().unchecked_ref()));
reader.set_onload(Some(on_load.as_ref().unchecked_ref()));
reader.read_as_array_buffer(file).unwrap();
on_cleanup(move || {
drop(on_progress);
drop(on_error);
drop(on_load);
});
}
}
});
let on_change = move |ev: Event| {
if let Some(target) = ev.target() {
let input = target.unchecked_into::<HtmlInputElement>();
if let Some(files) = input.files() {
if files.length() > 0 {
let file = FragileComfirmed::new(files.get(0).unwrap());
set_file.set(Some(file));
} else {
set_file.set(None);
}
}
}
};
let on_click = move |_| {
if let Some(input) = &*input_ref.read() {
if input.onchange().is_none() {
let callback = Closure::wrap(Box::new(on_change) as Box<dyn Fn(Event)>);
input.set_onchange(Some(callback.as_ref().unchecked_ref::<js_sys::Function>()));
callback.forget();
}
input.set_value("");
input.click();
}
};
view! { <Button on_click=on_click>"Load DB"</Button> }
}
#[component]
fn VfsMenuButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();

View File

@@ -13,7 +13,7 @@ pub fn Section(label: String, children: Children) -> impl IntoView {
view! {
<div>
<Header label=label />
{children()}
<p>{children()}</p>
</div>
}
}

View File

@@ -20,7 +20,7 @@ Please close other tabs and refresh, or switch to Memory VFS.";
pub fn Status() -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();
let show = move || match &*state.last_error().read() {
let last_error = move || match &*state.last_error().read() {
Some(error) => {
let summary = format!("{}", error.deref());
let details = match error.deref() {
@@ -42,12 +42,15 @@ pub fn Status() -> impl IntoView {
"An unsupported type was encountered, please create an issue on github."
}
},
WorkerError::NotFound | WorkerError::OpfsSAHError => {
WorkerError::NotFound | WorkerError::Unexpected => {
"This shouldn't happen, please create an issue on github."
}
WorkerError::InvaildState => {
"SQLite is in an abnormal state when executing SQLite."
}
WorkerError::LoadDb(_) => {
"Please check whether the imported DB is a SQLite3 file"
}
WorkerError::OpfsSAHPoolOpened => OPFS_SAH_POOL_OPENED_DETAILS,
},
SQLightError::AceEditor(ace_editor) => match ace_editor {
@@ -59,6 +62,9 @@ pub fn Status() -> impl IntoView {
"This shouldn't happen, please create an issue on github."
}
},
SQLightError::ImportDb(_) => {
"Maybe the db was not found, could not be read, or was too large."
}
};
view! {
@@ -72,9 +78,28 @@ pub fn Status() -> impl IntoView {
None => view! { "No Error" }.into_any(),
};
let import_progress = move || {
if let Some(progress) = &*state.import_progress().read() {
let filename = format!("Filename: {}", progress.filename);
let status = format!("Loading: {} of {} bytes.", progress.loaded, progress.total);
view! {
<p>{filename}</p>
<p>{status}</p>
}
.into_any()
} else {
view! { "No files are being imported." }.into_any()
}
};
view! {
<SimplePane>
<Section label="Last Error".into()>{show}</Section>
<Section label="Last Error".into()>
<pre style="white-space: pre-wrap;">{last_error}</pre>
</Section>
<Section label="Import Progress".into()>
<pre style="white-space: pre-wrap;">{import_progress}</pre>
</Section>
</SimplePane>
}
}

View File

@@ -37,6 +37,7 @@ pub fn playground(
handle_automic_orientation(state);
handle_connect_db(state);
hanlde_save_state(state);
handle_import_progress(state);
spawn_local(handle_state(state, rx));
@@ -83,6 +84,14 @@ fn handle_last_error(state: Store<GlobalState>) {
});
}
fn handle_import_progress(state: Store<GlobalState>) {
Effect::new(move || {
if state.import_progress().read().is_some() {
change_focus(state, Some(Focus::Status));
}
});
}
fn handle_system_theme(state: Store<GlobalState>) {
Effect::new(move || {
let theme = match state.theme().read().value() {

View File

@@ -41,6 +41,8 @@ pub struct GlobalState {
output: Vec<SQLiteStatementResult>,
#[serde(skip)]
last_error: Option<FragileComfirmed<SQLightError>>,
#[serde(skip)]
import_progress: Option<ImportProgress>,
}
impl Default for GlobalState {
@@ -62,6 +64,7 @@ impl Default for GlobalState {
show_something: false,
output: Vec::new(),
last_error: None,
import_progress: None,
}
}
}
@@ -85,6 +88,12 @@ impl GlobalState {
}
}
pub struct ImportProgress {
pub filename: String,
pub loaded: f64,
pub total: f64,
}
#[derive(Serialize, Deserialize)]
pub struct EditorConfig {
pub keyboard: String,

View File

@@ -6,7 +6,7 @@ use web_sys::{DedicatedWorkerGlobalScope, MessageEvent};
fn main() {
console_error_panic_hook::set_once();
console_log::init_with_level(log::Level::Warn).unwrap();
console_log::init_with_level(log::Level::Debug).unwrap();
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<JsValue>();
@@ -34,6 +34,9 @@ async fn execute_task(scope: DedicatedWorkerGlobalScope, mut rx: UnboundedReceiv
WorkerRequest::StepOver(id) => WorkerResponse::StepOver(worker::step_over(&id)),
WorkerRequest::StepIn(id) => WorkerResponse::StepIn(worker::step_in(&id)),
WorkerRequest::StepOut(id) => WorkerResponse::StepOut(worker::step_out(&id)),
WorkerRequest::LoadDb(options) => {
WorkerResponse::LoadDb(worker::load_db(options).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

@@ -4,6 +4,7 @@ pub mod worker;
use aceditor::EditorError;
use app::{GlobalState, GlobalStateStoreFields};
use fragile::Fragile;
use js_sys::Uint8Array;
use leptos::prelude::*;
use reactive_stores::Store;
use std::{
@@ -58,6 +59,8 @@ pub enum SQLightError {
Worker(#[from] WorkerError),
#[error(transparent)]
AceEditor(#[from] EditorError),
#[error("Failed to import db: {0}")]
ImportDb(String),
}
impl SQLightError {
@@ -80,8 +83,10 @@ pub enum WorkerError {
InvaildState,
#[error("OPFS already opened")]
OpfsSAHPoolOpened,
#[error("OPFS unexpected error")]
OpfsSAHError,
#[error("Failed to import db: {0}")]
LoadDb(String),
#[error("Unexpected error")]
Unexpected,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -92,6 +97,7 @@ pub enum WorkerRequest {
StepOver(String),
StepIn(String),
StepOut(String),
LoadDb(LoadDbOptions),
}
#[derive(Debug, Serialize, Deserialize)]
@@ -103,6 +109,7 @@ pub enum WorkerResponse {
StepOver(Result<SQLiteStatementResult>),
StepIn(Result<()>),
StepOut(Result<SQLiteStatementResult>),
LoadDb(Result<()>),
}
#[derive(Debug, Serialize, Deserialize)]
@@ -111,6 +118,14 @@ pub struct OpenOptions {
pub persist: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoadDbOptions {
pub id: String,
pub persist: bool,
#[serde(with = "serde_wasm_bindgen::preserve")]
pub data: Uint8Array,
}
impl OpenOptions {
pub fn uri(&self) -> String {
format!(
@@ -158,11 +173,11 @@ pub struct SQLiteStatementValues {
pub enum SQLitendError {
#[error("An error occurred while converting a string to a CString")]
ToCStr,
#[error("An error occurred while opening the DB: {0:?}")]
#[error("An error occurred while opening the DB: {0:#?}")]
OpenDb(InnerError),
#[error("An error occurred while preparing stmt: {0:?}")]
#[error("An error occurred while preparing stmt: {0:#?}")]
Prepare(InnerError),
#[error("An error occurred while stepping to the next line: {0:?}")]
#[error("An error occurred while stepping to the next line: {0:#?}")]
Step(InnerError),
#[error("An error occurred while getting column name: {0}")]
GetColumnName(String),
@@ -239,6 +254,15 @@ pub async fn handle_state(state: Store<GlobalState>, mut rx: UnboundedReceiver<W
WorkerResponse::StepOver(_)
| WorkerResponse::StepIn(_)
| WorkerResponse::StepOut(_) => unimplemented!(),
WorkerResponse::LoadDb(result) => {
let keep_ctx = result.is_ok();
if let Err(err) = result {
state.last_error().set(Some(SQLightError::new_worker(err)));
}
state
.keep_ctx()
.maybe_update(|keep| std::mem::replace(keep, keep_ctx) != keep_ctx);
}
}
}
}

View File

@@ -1,11 +1,14 @@
mod sqlitend;
use crate::{OpenOptions, PERSIST_VFS, PrepareOptions, SQLiteStatementResult, WorkerError};
use crate::{
LoadDbOptions, OpenOptions, PERSIST_VFS, PrepareOptions, SQLiteStatementResult, WorkerError,
};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use sqlite_wasm_rs::{
export::{OpfsSAHPoolCfgBuilder, OpfsSAHPoolUtil},
mem_vfs::MemVfsUtil,
utils::copy_to_vec,
};
use sqlitend::{SQLiteDb, SQLitePreparedStatement, SQLiteStatements};
use std::{collections::HashMap, sync::Arc};
@@ -50,12 +53,8 @@ where
f(DB_POOL.lock().get_mut(id).ok_or(WorkerError::NotFound)?)
}
pub async fn open(options: OpenOptions) -> Result<String> {
if let Some(worker) = DB_POOL.lock().get(&options.filename) {
return Ok(worker.id.clone());
}
if options.persist {
let util = FS_UTIL
async fn opfs_util() -> Result<&'static OpfsSAHPoolUtil> {
FS_UTIL
.opfs
.get_or_try_init(|| async {
sqlite_wasm_rs::sahpool_vfs::install(
@@ -70,11 +69,47 @@ pub async fn open(options: OpenOptions) -> Result<String> {
.await
.map_err(|_| WorkerError::OpfsSAHPoolOpened)
})
.await?;
.await
}
pub async fn load_db(options: LoadDbOptions) -> Result<()> {
let db = copy_to_vec(&options.data);
with_worker(&options.id, |worker| {
worker.db.take();
let filename = &worker.open_options.filename;
let FSUtil { mem, opfs } = &*FS_UTIL;
if worker.open_options.persist {
if let Some(opfs) = opfs.get() {
opfs.unlink(filename).map_err(|_| WorkerError::Unexpected)?;
if let Err(err) = opfs.import_db(filename, &db) {
return Err(WorkerError::LoadDb(format!("{err}")));
}
}
} else {
mem.delete_db(filename);
if let Err(err) = mem.import_db(filename, &db) {
return Err(WorkerError::LoadDb(format!("{err}")));
}
}
worker.db = Some(SQLiteDb::open(&worker.open_options.uri())?);
worker.state = SQLiteState::Idie;
Ok(())
})
}
pub async fn open(options: OpenOptions) -> Result<String> {
if let Some(worker) = DB_POOL.lock().get(&options.filename) {
return Ok(worker.id.clone());
}
if options.persist {
let util = opfs_util().await?;
if util.get_capacity() - util.get_file_count() * 3 < 3 {
util.add_capacity(3)
.await
.map_err(|_| WorkerError::OpfsSAHError)?;
.map_err(|_| WorkerError::Unexpected)?;
}
}
// FIXME: multi db support
@@ -99,8 +134,7 @@ pub fn prepare(options: PrepareOptions) -> Result<()> {
let FSUtil { mem, opfs } = &*FS_UTIL;
if worker.open_options.persist {
if let Some(opfs) = opfs.get() {
opfs.unlink(filename)
.map_err(|_| WorkerError::OpfsSAHError)?;
opfs.unlink(filename).map_err(|_| WorkerError::Unexpected)?;
}
} else {
mem.delete_db(filename);