feat: PoC for generating webpage with database
This commit is contained in:
parent
d7981129eb
commit
028cc115a0
8 changed files with 780 additions and 22 deletions
|
|
@ -25,6 +25,9 @@ home = "0.5"
|
||||||
|
|
||||||
# Required to transpile view functions to Rust
|
# Required to transpile view functions to Rust
|
||||||
genco = "0.15"
|
genco = "0.15"
|
||||||
|
# Required to generate fixture Elm files
|
||||||
|
elm-quote = { path = "../../../infra/rust-elm-quote" }
|
||||||
|
|
||||||
|
|
||||||
# All of these are required for deno's javascript runtime. We need to keep the
|
# All of these are required for deno's javascript runtime. We need to keep the
|
||||||
# same versions as other projects in our cargo workspace. Multiple different
|
# same versions as other projects in our cargo workspace. Multiple different
|
||||||
|
|
@ -45,3 +48,5 @@ serde_v8 = "0.15"
|
||||||
sqlx = { version = "0.5", features = [ "sqlite", "macros", "runtime-tokio-rustls", "chrono", "json", "uuid" ] }
|
sqlx = { version = "0.5", features = [ "sqlite", "macros", "runtime-tokio-rustls", "chrono", "json", "uuid" ] }
|
||||||
oneshot = "0.1.3"
|
oneshot = "0.1.3"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
aho-corasick = "0.7"
|
||||||
|
|
|
||||||
71
benches/string_replacement.rs
Normal file
71
benches/string_replacement.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
|
||||||
|
|
||||||
|
// TODO figure out why this code takes ~70ms to munge the javascript. By comparison just writing a
|
||||||
|
// bunch of chainged string.replace( , ).replace( , ).replace .... takes about 16ms.
|
||||||
|
let mut patterns = Vec::new();
|
||||||
|
let mut replace_with = Vec::new();
|
||||||
|
|
||||||
|
patterns.push("'REPLACE_ME_WITH_JSON_STRINGIFY'");
|
||||||
|
replace_with.push("JSON.stringify(x)");
|
||||||
|
|
||||||
|
patterns.push("$elm$json$Json$Decode$fail('REPLACE_ME_WITH_BYTES_DECODER');");
|
||||||
|
replace_with.push(r#"
|
||||||
|
_Json_decodePrim(function(value) {
|
||||||
|
return (typeof value === 'object' && value instanceof DataView)
|
||||||
|
? $elm$core$Result$Ok(value)
|
||||||
|
: _Json_expecting('a DataView', value);
|
||||||
|
});
|
||||||
|
"#);
|
||||||
|
|
||||||
|
patterns.push(";}(this));");
|
||||||
|
replace_with.push(";}(globalThis));");
|
||||||
|
|
||||||
|
|
||||||
|
// let mut final_script = data
|
||||||
|
|
||||||
|
if sqlite_path.is_some() {
|
||||||
|
patterns.push("var $author$project$Astrid$Query$execute = function (query) {\n\treturn $author$project$Astrid$Query$dummyExecute;\n};");
|
||||||
|
replace_with.push(include_str!("fixtures/sql-client-integration.js"));
|
||||||
|
|
||||||
|
patterns.push("var $author$project$Astrid$Query$fetch = F3(\n\tfunction (sql, parameters, decoder) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});");
|
||||||
|
replace_with.push("var $author$project$Astrid$Query$fetch = _Query_fetchAll;");
|
||||||
|
|
||||||
|
patterns.push("var $author$project$Astrid$Query$fetchOne = F3(\n\tfunction (sql, parameters, decoder) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});");
|
||||||
|
replace_with.push("var $author$project$Astrid$Query$fetchOne = _Query_fetchOne;");
|
||||||
|
|
||||||
|
patterns.push("var $author$project$Astrid$Query$map5 = F6(\n\tfunction (f, a, b, c, d, e) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});");
|
||||||
|
replace_with.push(r#"var $author$project$Astrid$Query$map5 = _Query_map5;"#);
|
||||||
|
|
||||||
|
patterns.push("var $author$project$Astrid$Query$map4 = F5(\n\tfunction (f, a, b, c, d) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});");
|
||||||
|
replace_with.push(r#"var $author$project$Astrid$Query$map4 = _Query_map4;"#);
|
||||||
|
|
||||||
|
patterns.push("var $author$project$Astrid$Query$map3 = F4(\n\tfunction (f, a, b, c) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});");
|
||||||
|
replace_with.push(r#"var $author$project$Astrid$Query$map3 = _Query_map3;"#);
|
||||||
|
|
||||||
|
patterns.push("var $author$project$Astrid$Query$map2 = F3(\n\tfunction (f, a, b) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});");
|
||||||
|
replace_with.push(r#"var $author$project$Astrid$Query$map2 = _Query_map2;"#);
|
||||||
|
|
||||||
|
patterns.push("var $author$project$Astrid$Query$map = F2(\n\tfunction (f, a) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});");
|
||||||
|
replace_with.push(r#"var $author$project$Astrid$Query$map = _Query_map1;"#);
|
||||||
|
|
||||||
|
patterns.push("var $author$project$Astrid$Query$andThen = F2(\n\tfunction (f, q) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});");
|
||||||
|
replace_with.push(r#"var $author$project$Astrid$Query$andThen = _Query_andThen;"#);
|
||||||
|
}
|
||||||
|
debug_assert!(patterns.len() == replace_with.len());
|
||||||
|
|
||||||
|
// let mut final_script = Vec::with_capacity(data.len() + 8 * 1024);
|
||||||
|
|
||||||
|
let span = info_span!("build aho-corasick patterns");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
//let ac = AhoCorasick::new(&patterns);
|
||||||
|
let ac = AhoCorasickBuilder::new()
|
||||||
|
.auto_configure(&patterns)
|
||||||
|
.build(&patterns);
|
||||||
|
drop(timing_guard);
|
||||||
|
let span = info_span!("run replacements");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
for _ in ac.find_iter(data.as_bytes()) {
|
||||||
|
|
||||||
|
}
|
||||||
|
drop(timing_guard);
|
||||||
|
let mut final_script = ac.replace_all_bytes(data.as_bytes(), &replace_with);
|
||||||
|
|
@ -1,18 +1,482 @@
|
||||||
use crate::reporting::{Problem};
|
use crate::exec::fixtures::astrid_pages::ScriptError;
|
||||||
use std::path::{PathBuf};
|
use crate::exec::{fixtures, runtime};
|
||||||
|
use crate::reporting::{CompilerError, InterpreterError, Problem, TypeError};
|
||||||
|
use deno_core::futures::StreamExt;
|
||||||
|
use elm_project_utils::{setup_generator_project, ElmResult};
|
||||||
|
use os_pipe::dup_stderr;
|
||||||
|
use rusty_v8 as v8;
|
||||||
|
use sqlx::sqlite::SqlitePool;
|
||||||
|
use sqlx::Row;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Instant;
|
||||||
|
use tokio;
|
||||||
|
use tracing::{info_span, Instrument};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum OutputType {
|
||||||
|
Html,
|
||||||
|
String,
|
||||||
|
Bytes,
|
||||||
|
Value,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn run(
|
pub(crate) fn run(
|
||||||
_debug: bool,
|
debug: bool,
|
||||||
_verbosity: u64,
|
verbosity: u64,
|
||||||
_sqlite_path: Option<PathBuf>,
|
sqlite_path: Option<PathBuf>,
|
||||||
_elm_project_dir: PathBuf,
|
elm_project_dir: PathBuf,
|
||||||
_source_checksum: u64,
|
source_checksum: u64,
|
||||||
_entrypoint: elmi::Global,
|
entrypoint: elmi::Global,
|
||||||
_output: Option<PathBuf>,
|
output: Option<PathBuf>,
|
||||||
) -> Result<(), Problem> {
|
) -> Result<(), Problem> {
|
||||||
// step 6 create our private project
|
// step 6 create our private project
|
||||||
// let generator_dir = setup_generator_project(verbosity, elm_project_dir.clone())?;
|
let packages = ["ThinkAlexandria/elm-html-in-elm", "elm/json", "elm/bytes"];
|
||||||
|
// I added a couple of random bytes to the directory name to reduce the risk of
|
||||||
|
// collisions with other programs that also use elm-stuff for their scratch space
|
||||||
|
let our_temp_dir = elm_project_dir.join("elm-stuff").join("starmelon-5d9ecc");
|
||||||
|
let generator_dir =
|
||||||
|
setup_generator_project(verbosity, elm_project_dir.clone(), our_temp_dir, &packages)?;
|
||||||
|
|
||||||
|
// TODO Step 7 find the flags module name
|
||||||
|
// TODO right now just hard code the module name and require projects to have `module Flags`
|
||||||
|
// I will actually detect the type and module after I get the first run a simple page proof of
|
||||||
|
// concept working
|
||||||
|
let flags_module = "Flags".to_owned();
|
||||||
|
|
||||||
|
// step 8 create an Elm fixture file to run our function
|
||||||
|
let span = info_span!("create Elm fixture files");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
// TODO actually detect the type of each route's output, and validate they are all something I
|
||||||
|
// can understand. I will need the interfaces dictionary for this.
|
||||||
|
let (gen_module_name, source) = fixtures::astrid_pages::generate(
|
||||||
|
source_checksum,
|
||||||
|
flags_module,
|
||||||
|
&[(entrypoint.0.module.clone().to_string(), OutputType::Html)],
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut source_filename = generator_dir.join("src").join(&gen_module_name);
|
||||||
|
source_filename.set_extension("elm");
|
||||||
|
|
||||||
|
let span = info_span!("file writes");
|
||||||
|
let file_write_timing_guard = span.enter();
|
||||||
|
fs::File::create(&source_filename)
|
||||||
|
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_filename.clone()))?
|
||||||
|
.write_all(source.as_bytes())
|
||||||
|
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, source_filename.clone()))?;
|
||||||
|
drop(file_write_timing_guard);
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
// step 8 compile the fixture
|
||||||
|
let mut intermediate_file = generator_dir.join("obj").join(&gen_module_name);
|
||||||
|
intermediate_file.set_extension("js");
|
||||||
|
|
||||||
|
let mut command = Command::new("elm");
|
||||||
|
|
||||||
|
command
|
||||||
|
.arg("make")
|
||||||
|
.arg("--output")
|
||||||
|
.arg(&intermediate_file)
|
||||||
|
.current_dir(&generator_dir)
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stderr(Stdio::piped());
|
||||||
|
if verbosity < 1 {
|
||||||
|
command.stdout(Stdio::piped());
|
||||||
|
} else {
|
||||||
|
let pipe = dup_stderr()
|
||||||
|
.map_err(|io_err| CompilerError::ReadInputFailed(io_err, "stdout".into()))?;
|
||||||
|
command.stdout(pipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
command.arg("--debug");
|
||||||
|
}
|
||||||
|
command.arg(&source_filename);
|
||||||
|
|
||||||
|
let span = info_span!("elm make fixture file");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
match command.output() {
|
||||||
|
Ok(output) => {
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(CompilerError::FailedBuildingFixture.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
return Err(CompilerError::FailedBuildingFixture.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
let data = fs::read_to_string(&intermediate_file)
|
||||||
|
.map_err(|io_err| CompilerError::ReadInputFailed(io_err, intermediate_file.clone()))?;
|
||||||
|
|
||||||
|
// TODO figure out how to replace multiple substrings in a single pass. One neat trick
|
||||||
|
// might be to allocate enough space in our starting buffer to write the new code by
|
||||||
|
// having a really large replace block. For example if we are replacing a string
|
||||||
|
// `"REPLACE_ME" + "abc" * 2000` we would have over 6k bytes to write out the new code. We
|
||||||
|
// will have to do some extra book keeping to make sure the buffer space is big enough
|
||||||
|
// for the replacement code.
|
||||||
|
let span = info_span!("munge fixture javascript");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
let final_script = (|| {
|
||||||
|
let mut final_script = data
|
||||||
|
.replace("'REPLACE_ME_WITH_JSON_STRINGIFY'", "JSON.stringify(x)")
|
||||||
|
.replace(
|
||||||
|
"$elm$json$Json$Decode$fail('REPLACE_ME_WITH_BYTES_DECODER');",
|
||||||
|
r#" _Json_decodePrim(function(value) {
|
||||||
|
return (typeof value === 'object' && value instanceof DataView)
|
||||||
|
? $elm$core$Result$Ok(value)
|
||||||
|
: _Json_expecting('a DataView', value);
|
||||||
|
});"#,
|
||||||
|
)
|
||||||
|
.replace(";}(this));", ";}(globalThis));");
|
||||||
|
|
||||||
|
if sqlite_path.is_some() {
|
||||||
|
final_script = final_script
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$execute = function (query) {\n\treturn $author$project$Astrid$Query$dummyExecute;\n};",
|
||||||
|
include_str!("fixtures/sql-client-integration.js"),
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$fetch = F3(\n\tfunction (sql, parameters, decoder) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
"var $author$project$Astrid$Query$fetch = _Query_fetchAll;",
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$fetchOne = F3(\n\tfunction (sql, parameters, decoder) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
"var $author$project$Astrid$Query$fetchOne = _Query_fetchOne;",
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$map5 = F6(\n\tfunction (f, a, b, c, d, e) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
r#"var $author$project$Astrid$Query$map5 = _Query_map5;"#,
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$map4 = F5(\n\tfunction (f, a, b, c, d) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
r#"var $author$project$Astrid$Query$map4 = _Query_map4;"#,
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$map3 = F4(\n\tfunction (f, a, b, c) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
r#"var $author$project$Astrid$Query$map3 = _Query_map3;"#,
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$map2 = F3(\n\tfunction (f, a, b) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
r#"var $author$project$Astrid$Query$map2 = _Query_map2;"#,
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$map = F2(\n\tfunction (f, a) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
r#"var $author$project$Astrid$Query$map = _Query_map1;"#,
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"var $author$project$Astrid$Query$andThen = F2(\n\tfunction (f, q) {\n\t\treturn $author$project$Astrid$Query$Dummy;\n\t});",
|
||||||
|
r#"var $author$project$Astrid$Query$andThen = _Query_andThen;"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
final_script.push_str("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
final_script.push_str("\n\n");
|
||||||
|
// I think that when I set this script to be the main module, I am skipping the
|
||||||
|
// deno/runtime/js/99_main.js script that sets up a bunch of global variables. If I
|
||||||
|
// manually add the timer related code below then setTimeout works again.
|
||||||
|
// NB. there are 706 lines of setup code that add a bunch of apis to the global window
|
||||||
|
// scope. Figure out if I need to include all of them. For example, starmelon does not need
|
||||||
|
// to perform http calls right now, but I eventually want to.
|
||||||
|
final_script.push_str("const { setTimeout } = globalThis.__bootstrap.timers;\n");
|
||||||
|
final_script.push_str(
|
||||||
|
"Deno.core.setMacrotaskCallback(globalThis.__bootstrap.timers.handleTimerMacrotask);\n",
|
||||||
|
);
|
||||||
|
final_script.push_str("globalThis.setTimeout = setTimeout;\n");
|
||||||
|
|
||||||
|
final_script.push_str(&format!(
|
||||||
|
"var worker = Elm.{}.init({{flags: {{ stagename: \"Atago\"}} }});\n",
|
||||||
|
&gen_module_name
|
||||||
|
));
|
||||||
|
// add a shortcut for invoking the function so I don't have to traverse so many object
|
||||||
|
// lookups using the rust v8 API.
|
||||||
|
final_script.push_str(
|
||||||
|
"globalThis.runOnInput = function(route) { worker.ports.onRequest.send(route) };\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
final_script.push_str(
|
||||||
|
r#"
|
||||||
|
worker.ports.onStringOutput.subscribe(function(result) {
|
||||||
|
Deno.core.opSync('op_starmelon_string_output', result);
|
||||||
|
});
|
||||||
|
// Elm will send a DataView
|
||||||
|
if (worker.ports.onBytesOutput) {
|
||||||
|
worker.ports.onBytesOutput.subscribe(function(result){
|
||||||
|
if (result.$ === "Ok") {
|
||||||
|
const ui8 = new Uint8Array(result.a.buffer);
|
||||||
|
output.a = ui8;
|
||||||
|
}
|
||||||
|
Deno.core.opSync('op_starmelon_bytes_output', result)
|
||||||
|
});
|
||||||
|
}"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
final_script
|
||||||
|
})();
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
let desired_route = entrypoint.0.module.clone().to_string();
|
||||||
|
let foo = move |mut scope: rusty_v8::HandleScope| -> Result<(), InterpreterError> {
|
||||||
|
let scope = &mut scope;
|
||||||
|
let ctx = scope.get_current_context();
|
||||||
|
let global = ctx.global(scope);
|
||||||
|
|
||||||
|
let entrypoint = {
|
||||||
|
let x =
|
||||||
|
v8::String::new(scope, "runOnInput").ok_or(InterpreterError::AllocationFailed)?;
|
||||||
|
v8::Local::new(scope, x).into()
|
||||||
|
};
|
||||||
|
let v8_value = global
|
||||||
|
.get(scope, entrypoint)
|
||||||
|
.ok_or(InterpreterError::ReferenceError)?;
|
||||||
|
|
||||||
|
// step 12 invoke the function
|
||||||
|
let function = v8::Local::<v8::Function>::try_from(v8_value)?;
|
||||||
|
|
||||||
|
let this = v8::undefined(scope).into();
|
||||||
|
|
||||||
|
let span = info_span!("dispatch v8 call");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
let arg1 = {
|
||||||
|
let x =
|
||||||
|
v8::String::new(scope, &desired_route).ok_or(InterpreterError::AllocationFailed)?;
|
||||||
|
v8::Local::new(scope, x).into()
|
||||||
|
};
|
||||||
|
|
||||||
|
function.call(scope, this, &[arg1]);
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut final_file = generator_dir.join("bin").join(&gen_module_name);
|
||||||
|
final_file.set_extension("js");
|
||||||
|
let span = info_span!("file writes");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
std::fs::write(&final_file, final_script)
|
||||||
|
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, final_file.clone()))?;
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
// Create a tokio runtime before registering ops so we can block on futures inside sync ops
|
||||||
|
let span = info_span!("create tokio runtime");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
let sys = tokio::runtime::Builder::new_current_thread()
|
||||||
|
// The default number of additional threads for running blocking FnOnce is 512.
|
||||||
|
.max_blocking_threads(1)
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
// step 10 create a v8 isolate. We need to register a different callback depending on
|
||||||
|
// the output type (string or bytes)
|
||||||
|
|
||||||
|
let span = info_span!("create v8 isolate");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
let (mut worker, main_module) = runtime::setup_worker(&final_file.to_string_lossy())
|
||||||
|
.map_err(|err| InterpreterError::EventLoop(err))?;
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
let span = info_span!("register private api");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
let mailbox: Arc<RefCell<Option<Result<Vec<u8>, ScriptError>>>> = Arc::new(RefCell::new(None));
|
||||||
|
|
||||||
|
let mailbox_clone = Arc::clone(&mailbox);
|
||||||
|
worker.js_runtime.register_op(
|
||||||
|
"op_starmelon_bytes_output",
|
||||||
|
deno_core::op_sync(
|
||||||
|
move |_state, msg: ElmResult<deno_core::ZeroCopyBuf, ScriptError>, _: ()| {
|
||||||
|
if let Ok(mut mailbox) = mailbox_clone.try_borrow_mut() {
|
||||||
|
match msg {
|
||||||
|
ElmResult::Ok { a: buffer } => {
|
||||||
|
let slice: &[u8] = &buffer;
|
||||||
|
mailbox.replace(Ok(slice.to_owned()));
|
||||||
|
}
|
||||||
|
ElmResult::Err { a: err } => {
|
||||||
|
mailbox.replace(Err(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mailbox_clone = Arc::clone(&mailbox);
|
||||||
|
worker.js_runtime.register_op(
|
||||||
|
"op_starmelon_string_output",
|
||||||
|
deno_core::op_sync(move |_state, msg: ElmResult<String, ScriptError>, _: ()| {
|
||||||
|
if let Ok(mut mailbox) = mailbox_clone.try_borrow_mut() {
|
||||||
|
match msg {
|
||||||
|
ElmResult::Ok { a: s } => mailbox.replace(Ok(s.into_bytes())),
|
||||||
|
ElmResult::Err { a: err } => mailbox.replace(Err(err)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 10.B setup the sqlite database feature
|
||||||
|
let sql_background_thread_handle = if let Some(database_url) = sqlite_path {
|
||||||
|
// I want to construct the connection in the initial thread so I can tell if the connection
|
||||||
|
// failed
|
||||||
|
let db_pool = sys
|
||||||
|
.block_on(async { SqlitePool::connect(&database_url.to_string_lossy()).await })
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (worker_mailbox, rx) = std::sync::mpsc::channel::<(
|
||||||
|
oneshot::Sender<ElmResult<_, _>>,
|
||||||
|
Vec<(bool, String, Vec<String>)>,
|
||||||
|
)>();
|
||||||
|
|
||||||
|
let sql_worker_thread = std::thread::spawn(move || {
|
||||||
|
let worker = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
loop {
|
||||||
|
if let Ok((response, queries)) = rx.recv() {
|
||||||
|
// I am not sure if I should only work on one database task at a time, or
|
||||||
|
// submit as many takes as possible. Just spawning the future onto this
|
||||||
|
// exectutor does not seem to work, even though the docs say the thread pool
|
||||||
|
// will poll the future until it completes.
|
||||||
|
let db_pool_clone = db_pool.clone();
|
||||||
|
let span = info_span!("inside sql queries futures");
|
||||||
|
let f = async move {
|
||||||
|
let _start = Instant::now();
|
||||||
|
let db_pool = db_pool_clone;
|
||||||
|
let mut result: Vec<Vec<String>> = vec![];
|
||||||
|
let mut failure: Option<String> = None;
|
||||||
|
for (fetch_all, sql, _args) in queries {
|
||||||
|
let mut acc = Vec::new();
|
||||||
|
if fetch_all {
|
||||||
|
let mut stream = sqlx::query(&sql).fetch(&db_pool);
|
||||||
|
loop {
|
||||||
|
match stream.next().await {
|
||||||
|
None => break,
|
||||||
|
Some(Ok(row)) => {
|
||||||
|
match row.try_get::<String, _>(0) {
|
||||||
|
Ok(s) => acc.push(s),
|
||||||
|
// TODO set an error flag before returning this one
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some(Err(err)) => {
|
||||||
|
eprintln!("got fetch_all sql error {:?}", err);
|
||||||
|
failure = Some(err.to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push(acc);
|
||||||
|
} else {
|
||||||
|
match sqlx::query(&sql)
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await
|
||||||
|
.and_then(|row| row.try_get::<String, _>(0))
|
||||||
|
{
|
||||||
|
Ok(s) => result.push(vec![s]),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("got fetchOne sql error {:?}", err);
|
||||||
|
failure = Some(err.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failure.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(msg) = failure {
|
||||||
|
response.send(ElmResult::err(msg))
|
||||||
|
} else {
|
||||||
|
response.send(ElmResult::ok(result))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// I found it interesting that the runtime of the future from the viewpoint of
|
||||||
|
// tracing was around 230us for a trivial select 2 rows query, but walltime I
|
||||||
|
// measured was around 700us. So polling the future or waiting for file IO is
|
||||||
|
// more expensive than I thought.
|
||||||
|
worker.block_on(f.instrument(span)).unwrap();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let worker_mailbox_clone = worker_mailbox.clone();
|
||||||
|
|
||||||
|
worker.js_runtime.register_op(
|
||||||
|
"op_starmelon_batch_queries",
|
||||||
|
deno_core::op_sync(
|
||||||
|
move |_state, queries: Vec<(bool, String, Vec<String>)>, _: ()| {
|
||||||
|
let worker_mailbox = worker_mailbox_clone.clone();
|
||||||
|
let (sender, receiver) =
|
||||||
|
oneshot::channel::<ElmResult<Vec<Vec<String>>, String>>();
|
||||||
|
|
||||||
|
let span = info_span!("run sql");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
|
||||||
|
worker_mailbox.send((sender, queries)).unwrap();
|
||||||
|
let elm_result = receiver.recv().unwrap();
|
||||||
|
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
Ok(elm_result)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Some((worker_mailbox, sql_worker_thread))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.js_runtime.sync_ops_cache();
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
let span = info_span!("eval javascript");
|
||||||
|
let timing_guard = span.enter();
|
||||||
|
sys.block_on(async move { runtime::xyz(worker, main_module, foo).await })?;
|
||||||
|
drop(timing_guard);
|
||||||
|
|
||||||
|
// step 13 receive the callback
|
||||||
|
// If I understood which combination of run_event_loop was required to execute
|
||||||
|
// the javascript to completion then we should have something in our mailbox by
|
||||||
|
// now. Another way to do this would be with an Arc. This will panic if the
|
||||||
|
// mailbox is currently borrowed
|
||||||
|
match mailbox.replace(None) {
|
||||||
|
Some(Ok(buffer)) => match output {
|
||||||
|
None => {
|
||||||
|
io::stdout()
|
||||||
|
.write_all(&buffer)
|
||||||
|
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, "stdout".into()))?;
|
||||||
|
}
|
||||||
|
Some(filename) => {
|
||||||
|
let mut f = fs::File::create(&filename)
|
||||||
|
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, filename))?;
|
||||||
|
|
||||||
|
f.write_all(&buffer)
|
||||||
|
.map_err(|io_err| CompilerError::WriteOutputFailed(io_err, "stdout".into()))?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(Err(problem)) => {
|
||||||
|
println!("had a problem {:?}", problem);
|
||||||
|
}
|
||||||
|
None => println!("nothing in the mailbox"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((tx, thread_handle)) = sql_background_thread_handle {
|
||||||
|
drop(tx);
|
||||||
|
thread_handle.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
eprintln!("todo implement astrid pages Route mode");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
218
src/exec/fixtures/astrid_pages.rs
Normal file
218
src/exec/fixtures/astrid_pages.rs
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
use crate::exec::astrid_pages::OutputType;
|
||||||
|
use elm_quote::Tokens;
|
||||||
|
use genco::tokens::quoted;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// I manually made this derived deserialize implementation match the elm json encoder output. The
|
||||||
|
// elm json encoder is defined in this file below. `encodeFailure : Error -> Json.Encode.Value`
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "$")]
|
||||||
|
pub enum ScriptError {
|
||||||
|
FlagsDecodeFailed { a: String },
|
||||||
|
QueryFailed { a: String },
|
||||||
|
HtmlGenerationFailed { a: String },
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn generate(
|
||||||
|
source_checksum: u64,
|
||||||
|
flags_module: String,
|
||||||
|
target_modules: &[(String, OutputType)],
|
||||||
|
) -> (String, String) {
|
||||||
|
let tokens: Tokens = genco::quote! {
|
||||||
|
import Astrid.Pages exposing (Route(..))
|
||||||
|
import Astrid.Query
|
||||||
|
import Bytes exposing (Bytes)
|
||||||
|
import ElmHtml.InternalTypes exposing (decodeElmHtml)
|
||||||
|
import ElmHtml.ToString exposing (nodeToStringWithOptions, defaultFormatOptions)
|
||||||
|
import Html exposing (Html)
|
||||||
|
import Json.Decode as D
|
||||||
|
import Json.Encode as E
|
||||||
|
import Platform
|
||||||
|
|
||||||
|
-- START CUSTOMIZED PART
|
||||||
|
import #(flags_module) exposing (Flags)
|
||||||
|
#(for (target_module, _) in target_modules.iter() =>
|
||||||
|
import #(target_module)
|
||||||
|
#<push>
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatch route flags =
|
||||||
|
case route of
|
||||||
|
#(for (target_module, output_type) in target_modules.iter() =>
|
||||||
|
#(" ")#(quoted(target_module)) ->
|
||||||
|
#( match output_type {
|
||||||
|
OutputType::String => {
|
||||||
|
#(" ")evalRoute (onStringOutput << encodeString) flags #(target_module).route
|
||||||
|
}
|
||||||
|
OutputType::Value => {
|
||||||
|
#(" ")evalRoute (onStringOutput << encodeJson) flags #(target_module).route
|
||||||
|
}
|
||||||
|
OutputType::Bytes => {
|
||||||
|
#(" ")evalRoute (onStringOutput << encodeBytes) flags #(target_module).route
|
||||||
|
}
|
||||||
|
OutputType::Html => {
|
||||||
|
#(" ")evalRoute (onStringOutput << encodeHtml) flags #(target_module).route
|
||||||
|
}
|
||||||
|
})
|
||||||
|
#<push>
|
||||||
|
)
|
||||||
|
#<line>
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
onStringOutput (encodeFailure NotFound)
|
||||||
|
|
||||||
|
-- END CUSTOMIZED PART
|
||||||
|
|
||||||
|
-- MAIN
|
||||||
|
|
||||||
|
main =
|
||||||
|
Platform.worker
|
||||||
|
{ init = init
|
||||||
|
, update = update
|
||||||
|
, subscriptions = subscriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
-- PORTS
|
||||||
|
|
||||||
|
port onRequest : (String -> msg) -> Sub msg
|
||||||
|
port onStringOutput : E.Value -> Cmd msg
|
||||||
|
port onBytesOutput : E.Value -> Cmd msg
|
||||||
|
|
||||||
|
-- MODEL
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
init : Flags -> (Flags, Cmd Msg)
|
||||||
|
init flags =
|
||||||
|
(flags, Cmd.none)
|
||||||
|
|
||||||
|
-- UPDATE
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= DispatchRoute String
|
||||||
|
|
||||||
|
update : Msg -> Flags -> (Flags, Cmd Msg)
|
||||||
|
update msg flags =
|
||||||
|
case msg of
|
||||||
|
DispatchRoute route ->
|
||||||
|
let
|
||||||
|
cmd =
|
||||||
|
dispatch route flags
|
||||||
|
in
|
||||||
|
(flags, cmd)
|
||||||
|
|
||||||
|
evalRoute : (output -> Cmd msg) -> flags -> Route flags model output -> Cmd msg
|
||||||
|
evalRoute reportResponse flags (Route { handler, view }) =
|
||||||
|
case Astrid.Query.execute (handler flags) of
|
||||||
|
Ok model ->
|
||||||
|
reportResponse (view flags model)
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
onStringOutput (encodeFailure (QueryFailed (Astrid.Query.errorToString err)))
|
||||||
|
|
||||||
|
|
||||||
|
type Error
|
||||||
|
= FlagsDecodeFailed D.Error
|
||||||
|
| QueryFailed String
|
||||||
|
| HtmlGenerationFailed String
|
||||||
|
| NotFound
|
||||||
|
|
||||||
|
-- SUBSCRIPTIONS
|
||||||
|
|
||||||
|
subscriptions : model -> Sub Msg
|
||||||
|
subscriptions _ =
|
||||||
|
onRequest DispatchRoute
|
||||||
|
|
||||||
|
-- ENCODERS
|
||||||
|
|
||||||
|
encodeFailure : Error -> E.Value
|
||||||
|
encodeFailure err =
|
||||||
|
E.object
|
||||||
|
[ ("$", E.string "Err")
|
||||||
|
, ("a"
|
||||||
|
, case err of
|
||||||
|
FlagsDecodeFailed jsonError ->
|
||||||
|
E.object
|
||||||
|
[ ("$", E.string "FlagsDecodeFailed" )
|
||||||
|
, ("a", E.string (D.errorToString jsonError))
|
||||||
|
]
|
||||||
|
|
||||||
|
QueryFailed msg ->
|
||||||
|
E.object
|
||||||
|
[ ("$", E.string "QueryFailed" )
|
||||||
|
, ("a", E.string msg)
|
||||||
|
]
|
||||||
|
|
||||||
|
HtmlGenerationFailed msg ->
|
||||||
|
E.object
|
||||||
|
[ ("$", E.string "HtmlGenerationFailed" )
|
||||||
|
, ("a", E.string msg)
|
||||||
|
]
|
||||||
|
|
||||||
|
NotFound ->
|
||||||
|
E.object
|
||||||
|
[ ("$", E.string "NotFound" )
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
encodeSuccess : E.Value -> E.Value
|
||||||
|
encodeSuccess value =
|
||||||
|
E.object
|
||||||
|
[ ("ctor", E.string "Ok")
|
||||||
|
, ("a", value)
|
||||||
|
]
|
||||||
|
|
||||||
|
encodeJson : E.Value -> E.Value
|
||||||
|
encodeJson v =
|
||||||
|
encodeString (E.encode 0 v)
|
||||||
|
|
||||||
|
encodeBytes : Bytes -> E.Value
|
||||||
|
encodeBytes bytes =
|
||||||
|
bytes
|
||||||
|
|> encodeBytesHelp
|
||||||
|
|> encodeSuccess
|
||||||
|
|
||||||
|
encodeBytesHelp : Bytes -> E.Value
|
||||||
|
encodeBytesHelp bytes =
|
||||||
|
E.string "REPLACE_ME_WITH_BYTES_ENCODER"
|
||||||
|
|
||||||
|
encodeString : String -> E.Value
|
||||||
|
encodeString s =
|
||||||
|
encodeSuccess (E.string s)
|
||||||
|
|
||||||
|
encodeHtml : Html msg -> E.Value
|
||||||
|
encodeHtml node =
|
||||||
|
let
|
||||||
|
options =
|
||||||
|
{ defaultFormatOptions | newLines = True, indent = 2 }
|
||||||
|
in
|
||||||
|
case
|
||||||
|
D.decodeString
|
||||||
|
(decodeElmHtml ( #("\\")taggers eventHandler -> D.succeed ()))
|
||||||
|
(asJsonString node)
|
||||||
|
of
|
||||||
|
Ok elmHtml ->
|
||||||
|
elmHtml
|
||||||
|
|> nodeToStringWithOptions options
|
||||||
|
|> encodeString
|
||||||
|
|
||||||
|
Err error ->
|
||||||
|
encodeFailure (HtmlGenerationFailed (D.errorToString error))
|
||||||
|
|
||||||
|
|
||||||
|
asJsonString : Html msg -> String
|
||||||
|
asJsonString x = "REPLACE_ME_WITH_JSON_STRINGIFY"
|
||||||
|
};
|
||||||
|
|
||||||
|
let module_name = format!("Route{:020}", source_checksum);
|
||||||
|
|
||||||
|
let source = format!(
|
||||||
|
"port module {} exposing (main)\n\n{}",
|
||||||
|
module_name,
|
||||||
|
tokens.to_file_string().unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
(module_name, source)
|
||||||
|
}
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
pub mod astrid_pages;
|
||||||
pub mod scripting;
|
pub mod scripting;
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ use crate::elm;
|
||||||
use crate::reporting::{CompilerError, Problem};
|
use crate::reporting::{CompilerError, Problem};
|
||||||
use crate::PortableHash;
|
use crate::PortableHash;
|
||||||
use std::hash::Hasher;
|
use std::hash::Hasher;
|
||||||
use std::io::{Write};
|
use std::io::Write;
|
||||||
use std::path::{PathBuf};
|
use std::path::PathBuf;
|
||||||
use tracing::{info_span};
|
use tracing::info_span;
|
||||||
|
|
||||||
mod astrid_pages;
|
mod astrid_pages;
|
||||||
mod fixtures;
|
mod fixtures;
|
||||||
|
|
@ -127,9 +127,7 @@ mod runtime {
|
||||||
use deno_runtime::worker::WorkerOptions;
|
use deno_runtime::worker::WorkerOptions;
|
||||||
use deno_runtime::BootstrapOptions;
|
use deno_runtime::BootstrapOptions;
|
||||||
use deno_web::BlobStore;
|
use deno_web::BlobStore;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::exec::{fixtures, runtime};
|
||||||
use crate::reporting::{CompilerError, InterpreterError, Problem, TypeError};
|
use crate::reporting::{CompilerError, InterpreterError, Problem, TypeError};
|
||||||
use deno_core::futures::StreamExt;
|
use deno_core::futures::StreamExt;
|
||||||
use elm_project_utils::{setup_generator_project, ElmResult};
|
use elm_project_utils::{setup_generator_project, ElmResult};
|
||||||
|
|
@ -7,15 +8,14 @@ use sqlx::sqlite::SqlitePool;
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fs::{self};
|
use std::fs;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::path::{PathBuf};
|
use std::path::PathBuf;
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tokio;
|
use tokio;
|
||||||
use tracing::{info_span, Instrument};
|
use tracing::{info_span, Instrument};
|
||||||
use crate::exec::{fixtures, runtime};
|
|
||||||
|
|
||||||
pub(crate) fn run(
|
pub(crate) fn run(
|
||||||
debug: bool,
|
debug: bool,
|
||||||
|
|
|
||||||
|
|
@ -82,9 +82,10 @@ impl From<elm_project_utils::ProjectSetupError> for Problem {
|
||||||
ProjectSetupError::ReadFailed { filename, source } => {
|
ProjectSetupError::ReadFailed { filename, source } => {
|
||||||
CompilerError::ReadInputFailed(source, filename).into()
|
CompilerError::ReadInputFailed(source, filename).into()
|
||||||
}
|
}
|
||||||
ProjectSetupError::FailedParseElmJson { filename: _, source } => {
|
ProjectSetupError::FailedParseElmJson {
|
||||||
CompilerError::FailedParseElmJson(source).into()
|
filename: _,
|
||||||
}
|
source,
|
||||||
|
} => CompilerError::FailedParseElmJson(source).into(),
|
||||||
ProjectSetupError::InstallDependencyFailed => {
|
ProjectSetupError::InstallDependencyFailed => {
|
||||||
SetupError::InstallDependencyFailed.into()
|
SetupError::InstallDependencyFailed.into()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue