mirror of
https://github.com/trailbaseio/trailbase.git
synced 2025-12-16 15:15:51 -06:00
It's been a little over 6 weeks since v0.19 removing V8 from the trail binary. This change now removes the code.
I wasn't sure how long to keep the code around as an optional feature. However, the latest wasmtime release pins serde to a version, which is incompatible with the latest (somewhat stale) deno/V8/SWC dependencies. In other words, updating wasmtime right now would break the V8 build :/. The state of the rustyscript/deno dependency chain had certainly been one of the driving factors to move to WASM. While it feels validating, it's a little sad to see V8 go... but at the least we want to make the removal eplicit (as opposed to a broken build) with this change.
This commit is contained in:
3183
Cargo.lock
generated
3183
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ members = [
|
||||
"crates/client",
|
||||
"crates/core",
|
||||
"crates/extension",
|
||||
"crates/js-runtime",
|
||||
"crates/qs",
|
||||
"crates/refinery",
|
||||
"crates/schema",
|
||||
@@ -95,7 +94,6 @@ trailbase-assets = { path = "crates/assets", version = "0.2.0" }
|
||||
trailbase-build = { path = "crates/build", version = "0.1.1" }
|
||||
trailbase-client = { path = "crates/client", version = "0.5.0" }
|
||||
trailbase-extension = { path = "crates/extension", version = "0.3.0" }
|
||||
trailbase-js = { path = "crates/js-runtime", version = "0.2.0" }
|
||||
trailbase-qs = { path = "crates/qs", version = "0.1.0" }
|
||||
trailbase-refinery = { path = "crates/refinery", version = "0.1.0" }
|
||||
trailbase-schema = { path = "crates/schema", version = "0.1.0" }
|
||||
|
||||
@@ -13,8 +13,6 @@ name = "trail"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Conditionally enable "v8" feature of dep:trailbase.
|
||||
v8 = ["trailbase/v8"]
|
||||
swagger = ["dep:utoipa-swagger-ui"]
|
||||
vendor-ssl = ["dep:openssl"]
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ harness = false
|
||||
|
||||
[features]
|
||||
default = ["wasm"]
|
||||
v8 = ["dep:trailbase-js"]
|
||||
wasm = ["dep:trailbase-wasm-runtime-host"]
|
||||
otel = ["dep:axum-tracing-opentelemetry", "dep:init-tracing-opentelemetry"]
|
||||
|
||||
@@ -93,7 +92,6 @@ tracing-subscriber = { workspace = true }
|
||||
trailbase-assets = { workspace = true }
|
||||
trailbase-build = { workspace = true }
|
||||
trailbase-extension = { workspace = true }
|
||||
trailbase-js = { workspace = true, optional = true }
|
||||
trailbase-qs = { workspace = true }
|
||||
trailbase-refinery = { workspace = true }
|
||||
trailbase-schema = { workspace = true }
|
||||
|
||||
@@ -48,9 +48,6 @@ struct InternalState {
|
||||
subscription_manager: SubscriptionManager,
|
||||
object_store: Arc<dyn ObjectStore + Send + Sync>,
|
||||
|
||||
#[cfg(feature = "v8")]
|
||||
runtime: crate::js::RuntimeHandle,
|
||||
|
||||
/// Actual WASM runtimes.
|
||||
wasm_runtimes: Vec<Arc<RwLock<Runtime>>>,
|
||||
/// WASM runtime builders needed to rebuild above runtimes, e.g. when hot-reloading.
|
||||
@@ -204,8 +201,6 @@ impl AppState {
|
||||
),
|
||||
connection_metadata,
|
||||
object_store,
|
||||
#[cfg(feature = "v8")]
|
||||
runtime: build_js_runtime(args.conn.clone(), args.runtime_threads),
|
||||
wasm_runtimes: build_wasm_runtime()
|
||||
.expect("startup")
|
||||
.into_iter()
|
||||
@@ -400,11 +395,6 @@ impl AppState {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "v8")]
|
||||
pub(crate) fn script_runtime(&self) -> crate::js::RuntimeHandle {
|
||||
return self.state.runtime.clone();
|
||||
}
|
||||
|
||||
pub(crate) fn wasm_runtimes(&self) -> &[Arc<RwLock<Runtime>>] {
|
||||
return &self.state.wasm_runtimes;
|
||||
}
|
||||
@@ -641,8 +631,6 @@ pub async fn test_state(options: Option<TestStateOptions>) -> anyhow::Result<App
|
||||
),
|
||||
connection_metadata,
|
||||
object_store,
|
||||
#[cfg(feature = "v8")]
|
||||
runtime: build_js_runtime(conn, None),
|
||||
wasm_runtimes: vec![],
|
||||
build_wasm_runtimes: Box::new(|| Ok(vec![])),
|
||||
test_cleanup: vec![Box::new(temp_dir)],
|
||||
@@ -668,33 +656,6 @@ where
|
||||
return derived;
|
||||
}
|
||||
|
||||
#[cfg(feature = "v8")]
|
||||
fn build_js_runtime(
|
||||
conn: trailbase_sqlite::Connection,
|
||||
threads: Option<usize>,
|
||||
) -> crate::js::RuntimeHandle {
|
||||
use crate::js::{RuntimeHandle, register_database_functions};
|
||||
|
||||
let runtime = if let Some(threads) = threads {
|
||||
RuntimeHandle::singleton_or_init_with_threads(threads)
|
||||
} else {
|
||||
RuntimeHandle::singleton()
|
||||
};
|
||||
|
||||
if cfg!(test) {
|
||||
lazy_static::lazy_static! {
|
||||
static ref START: std::sync::Once = std::sync::Once::new();
|
||||
}
|
||||
START.call_once(|| {
|
||||
register_database_functions(&runtime, conn);
|
||||
});
|
||||
} else {
|
||||
register_database_functions(&runtime, conn);
|
||||
}
|
||||
|
||||
return runtime;
|
||||
}
|
||||
|
||||
fn build_record_api(
|
||||
conn: trailbase_sqlite::Connection,
|
||||
connection_metadata: &ConnectionMetadata,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#[cfg(feature = "v8")]
|
||||
pub(crate) mod runtime;
|
||||
|
||||
#[cfg(feature = "v8")]
|
||||
pub use trailbase_js::runtime::{RuntimeHandle, register_database_functions};
|
||||
@@ -1,385 +0,0 @@
|
||||
use axum::Router;
|
||||
use axum::body::Body;
|
||||
use axum::extract::{RawPathParams, Request};
|
||||
use axum::http::{HeaderName, HeaderValue, request::Parts};
|
||||
use axum::http::{StatusCode, header::CONTENT_TYPE};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use futures_util::FutureExt;
|
||||
use log::*;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use trailbase_js::runtime::{
|
||||
JsUser, LargeRSError, Message, Module, Runtime, RuntimeHandle,
|
||||
build_call_async_js_function_message, get_arg,
|
||||
};
|
||||
|
||||
use crate::AppState;
|
||||
use crate::auth::user::User;
|
||||
|
||||
type AnyError = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
pub struct DispatchArgs {
|
||||
pub method: String,
|
||||
pub route_path: String,
|
||||
pub uri: String,
|
||||
pub path_params: Vec<(String, String)>,
|
||||
pub headers: Vec<(String, String)>,
|
||||
pub user: Option<JsUser>,
|
||||
pub body: bytes::Bytes,
|
||||
|
||||
pub reply: oneshot::Sender<Result<JsHttpResponse, Box<LargeRSError>>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
pub struct JsHttpResponse {
|
||||
pub headers: Option<Vec<(String, String)>>,
|
||||
pub status: Option<u16>,
|
||||
pub body: Option<bytes::Bytes>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum JsHttpResponseError {
|
||||
#[error("Precondition: {0}")]
|
||||
Precondition(String),
|
||||
#[error("Internal: {0}")]
|
||||
Internal(Box<dyn std::error::Error + Send + Sync>),
|
||||
#[error("Runtime: {0}")]
|
||||
Runtime(#[from] Box<LargeRSError>),
|
||||
}
|
||||
|
||||
impl IntoResponse for JsHttpResponseError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, body): (StatusCode, Option<String>) = match self {
|
||||
Self::Precondition(err) => (StatusCode::PRECONDITION_FAILED, Some(err.to_string())),
|
||||
Self::Internal(err) => (StatusCode::INTERNAL_SERVER_ERROR, Some(err.to_string())),
|
||||
Self::Runtime(err) => (StatusCode::INTERNAL_SERVER_ERROR, Some(err.to_string())),
|
||||
};
|
||||
|
||||
if let Some(body) = body {
|
||||
return Response::builder()
|
||||
.status(status)
|
||||
.header(CONTENT_TYPE, "text/plain")
|
||||
.body(Body::new(body))
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
return Response::builder()
|
||||
.status(status)
|
||||
.body(Body::empty())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get's called from JS during `addRoute` and installs an axum HTTP handler.
|
||||
///
|
||||
/// The axum HTTP handler will then call back into the registered callback in JS.
|
||||
fn add_route_to_router(
|
||||
runtime_handle: RuntimeHandle,
|
||||
method: String,
|
||||
route: String,
|
||||
) -> Result<Router<AppState>, AnyError> {
|
||||
let method_uppercase = method.to_uppercase();
|
||||
|
||||
let route_path = route.clone();
|
||||
let handler = move |params: RawPathParams, user: Option<User>, req: Request| async move {
|
||||
let (parts, body) = req.into_parts();
|
||||
|
||||
let Ok(body_bytes) = axum::body::to_bytes(body, usize::MAX).await else {
|
||||
return Err(JsHttpResponseError::Precondition(
|
||||
"request deserialization failed".to_string(),
|
||||
));
|
||||
};
|
||||
let Parts { uri, headers, .. } = parts;
|
||||
|
||||
let path_params: Vec<(String, String)> = params
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect();
|
||||
let headers: Vec<(String, String)> = headers
|
||||
.into_iter()
|
||||
.filter_map(|(key, value)| {
|
||||
if let Some(key) = key
|
||||
&& let Ok(value) = value.to_str()
|
||||
{
|
||||
return Some((key.to_string(), value.to_string()));
|
||||
}
|
||||
return None;
|
||||
})
|
||||
.collect();
|
||||
|
||||
let js_user: Option<JsUser> = user.map(|u| JsUser {
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
csrf: u.csrf_token,
|
||||
});
|
||||
|
||||
let (sender, receiver) = oneshot::channel::<Result<JsHttpResponse, Box<LargeRSError>>>();
|
||||
|
||||
debug!("dispatch {method} {uri}");
|
||||
runtime_handle
|
||||
.send_to_any_isolate(build_http_dispatch_message(DispatchArgs {
|
||||
method,
|
||||
route_path,
|
||||
uri: uri.to_string(),
|
||||
path_params,
|
||||
headers,
|
||||
user: js_user,
|
||||
body: body_bytes,
|
||||
reply: sender,
|
||||
}))
|
||||
.await
|
||||
.map_err(|_err| JsHttpResponseError::Internal("send failed".into()))?;
|
||||
|
||||
let js_response = receiver
|
||||
.await
|
||||
.map_err(|_err| JsHttpResponseError::Internal("receive failed".into()))??;
|
||||
|
||||
let mut http_response = Response::builder()
|
||||
.status(js_response.status.unwrap_or(200))
|
||||
.body(Body::from(js_response.body.unwrap_or_default()))
|
||||
.map_err(|err| JsHttpResponseError::Internal(err.into()))?;
|
||||
|
||||
if let Some(headers) = js_response.headers {
|
||||
for (key, value) in headers {
|
||||
http_response.headers_mut().insert(
|
||||
HeaderName::from_str(key.as_str())
|
||||
.map_err(|err| JsHttpResponseError::Internal(err.into()))?,
|
||||
HeaderValue::from_str(value.as_str())
|
||||
.map_err(|err| JsHttpResponseError::Internal(err.into()))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(http_response);
|
||||
};
|
||||
|
||||
return Ok(Router::<AppState>::new().route(
|
||||
&route,
|
||||
match method_uppercase.as_str() {
|
||||
"DELETE" => axum::routing::delete(handler),
|
||||
"GET" => axum::routing::get(handler),
|
||||
"HEAD" => axum::routing::head(handler),
|
||||
"OPTIONS" => axum::routing::options(handler),
|
||||
"PATCH" => axum::routing::patch(handler),
|
||||
"POST" => axum::routing::post(handler),
|
||||
"PUT" => axum::routing::put(handler),
|
||||
"TRACE" => axum::routing::trace(handler),
|
||||
_ => {
|
||||
return Err(format!("method: {method_uppercase}").into());
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
async fn install_routes_and_jobs(
|
||||
state: &AppState,
|
||||
module: Module,
|
||||
) -> Result<Option<Router<AppState>>, AnyError> {
|
||||
let runtime_handle = state.script_runtime();
|
||||
let jobs = state.jobs();
|
||||
|
||||
// For all the isolates/worker-threads.
|
||||
let receivers: Vec<_> = runtime_handle
|
||||
.state()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(async |(index, state)| {
|
||||
let module = module.clone();
|
||||
let runtime_handle = runtime_handle.clone();
|
||||
let jobs = jobs.clone();
|
||||
|
||||
let (router_sender, router_receiver) = kanal::unbounded::<Router<AppState>>();
|
||||
|
||||
if let Err(err) = state
|
||||
.send_privately(Message::Run(
|
||||
None,
|
||||
Box::new(move |_m, runtime: &mut Runtime| {
|
||||
// First install a native callbacks.
|
||||
//
|
||||
// Register native callback for building axum router.
|
||||
let runtime_handle_clone = runtime_handle.clone();
|
||||
runtime
|
||||
.register_function("install_route", move |args: &[serde_json::Value]| {
|
||||
let method: String = get_arg(args, 0)?;
|
||||
let route: String = get_arg(args, 1)?;
|
||||
|
||||
let router = add_route_to_router(runtime_handle_clone.clone(), method, route)
|
||||
.map_err(|err| LargeRSError::Runtime(err.to_string()))?;
|
||||
|
||||
router_sender.send(router).expect("send");
|
||||
|
||||
return Ok(serde_json::Value::Null);
|
||||
})
|
||||
.expect("Failed to register 'install_route' function");
|
||||
|
||||
// Register native callback for registering cron jobs.
|
||||
runtime
|
||||
.register_function(
|
||||
"install_job",
|
||||
move |args: &[serde_json::Value]| -> Result<serde_json::Value, _> {
|
||||
let name: String = get_arg(args, 0)?;
|
||||
let default_spec: String = get_arg(args, 1)?;
|
||||
let schedule = cron::Schedule::from_str(&default_spec).map_err(|err| {
|
||||
return LargeRSError::Runtime(err.to_string());
|
||||
})?;
|
||||
|
||||
let runtime_handle = runtime_handle.clone();
|
||||
let (id_sender, id_receiver) = oneshot::channel::<i64>();
|
||||
let id_receiver = id_receiver.shared();
|
||||
|
||||
let Some(job) = jobs.new_job(
|
||||
None,
|
||||
name,
|
||||
schedule,
|
||||
crate::scheduler::build_callback(move || {
|
||||
let runtime_handle = runtime_handle.clone();
|
||||
let id_receiver = id_receiver.clone();
|
||||
|
||||
return async move {
|
||||
let Some(first_isolate) = runtime_handle.state().first() else {
|
||||
return Err("Missing isolate".into());
|
||||
};
|
||||
|
||||
let (sender, receiver) =
|
||||
oneshot::channel::<Result<Option<String>, Box<LargeRSError>>>();
|
||||
let id = id_receiver.await?;
|
||||
first_isolate
|
||||
.send_privately(build_call_async_js_function_message::<Option<String>>(
|
||||
None,
|
||||
"__dispatchCron",
|
||||
[id],
|
||||
sender,
|
||||
))
|
||||
.await?;
|
||||
|
||||
match receiver.await? {
|
||||
Err(err) => debug!("cron failed: {err}"),
|
||||
Ok(Some(err)) => debug!("cron failed: {err}"),
|
||||
_ => {}
|
||||
};
|
||||
|
||||
Ok::<_, AnyError>(())
|
||||
};
|
||||
}),
|
||||
) else {
|
||||
return Err(LargeRSError::Runtime("Failed to add job".to_string()));
|
||||
};
|
||||
|
||||
if let Err(err) = id_sender.send(job.id as i64) {
|
||||
return Err(LargeRSError::Runtime(err.to_string()));
|
||||
}
|
||||
|
||||
job.start();
|
||||
|
||||
return Ok(job.id.into());
|
||||
},
|
||||
)
|
||||
.expect("Failed to register 'install_job' function");
|
||||
|
||||
return None;
|
||||
}),
|
||||
))
|
||||
.await
|
||||
{
|
||||
panic!("Failed to comm with v8 rt'{index}': {err}");
|
||||
}
|
||||
|
||||
// Then execute the script/module, i.e. statements in the file scope.
|
||||
if let Err(err) = state.load_module(module).await {
|
||||
error!("Failed to load module: {err}");
|
||||
return None;
|
||||
}
|
||||
|
||||
// Now all module-level calls to `install_route` should have happened. Let's drain the
|
||||
// registered routes. Note, we cannot `collect()` since the sender side never hangs up.
|
||||
let mut installed_routers: Vec<Router<AppState>> = vec![];
|
||||
match router_receiver.drain_into(&mut installed_routers) {
|
||||
Ok(n) => debug!("Got {n} routers from JS"),
|
||||
Err(err) => {
|
||||
error!("Failed to get routers from JS: {err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let mut merged_router = Router::<AppState>::new();
|
||||
for router in installed_routers {
|
||||
if router.has_routes() {
|
||||
merged_router = merged_router.merge(router);
|
||||
}
|
||||
}
|
||||
return Some(merged_router);
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Await function registration and module loading for all isolates/worker-threads.
|
||||
let mut receivers = futures_util::future::join_all(receivers).await;
|
||||
|
||||
// Note: We only return the first router assuming that JS route registration is consistent across
|
||||
// all isolates.
|
||||
return Ok(receivers.swap_remove(0));
|
||||
}
|
||||
|
||||
pub(crate) async fn load_routes_and_jobs_from_js_modules(
|
||||
state: &AppState,
|
||||
scripts_dir: PathBuf,
|
||||
) -> Result<Option<Router<AppState>>, AnyError> {
|
||||
let runtime_handle = state.script_runtime();
|
||||
if runtime_handle.num_threads() == 0 {
|
||||
info!("JS threads set to zero. Skipping initialization for JS modules");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let modules = match Module::load_dir(scripts_dir.clone()) {
|
||||
Ok(modules) => modules,
|
||||
Err(err) => {
|
||||
debug!("Skip loading js modules from '{scripts_dir:?}': {err}");
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
if !modules.is_empty() {
|
||||
warn!(
|
||||
"Found JS/TS scripts. The V8 runtime is deprecated and will likely be \
|
||||
removed in the next major release. Please migrate to WASM. If you have \
|
||||
concerns or encounter any issues, don't hesitate to reach out."
|
||||
);
|
||||
}
|
||||
|
||||
let mut js_router = Router::new();
|
||||
for module in modules {
|
||||
let fname = module.filename().to_owned();
|
||||
|
||||
if let Some(router) = install_routes_and_jobs(state, module).await? {
|
||||
js_router = js_router.merge(router);
|
||||
} else {
|
||||
debug!("Skipping js module '{fname:?}': no routes");
|
||||
}
|
||||
}
|
||||
|
||||
if js_router.has_routes() {
|
||||
return Ok(Some(js_router));
|
||||
}
|
||||
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
pub fn build_http_dispatch_message(args: DispatchArgs) -> Message {
|
||||
return build_call_async_js_function_message(
|
||||
None,
|
||||
"__dispatch",
|
||||
serde_json::json!([
|
||||
args.method,
|
||||
args.route_path,
|
||||
args.uri,
|
||||
args.path_params,
|
||||
args.headers,
|
||||
args.user,
|
||||
args.body
|
||||
]),
|
||||
args.reply,
|
||||
);
|
||||
}
|
||||
@@ -17,7 +17,6 @@ mod data_dir;
|
||||
mod email;
|
||||
mod encryption;
|
||||
mod extract;
|
||||
mod js;
|
||||
mod listing;
|
||||
mod migrations;
|
||||
mod scheduler;
|
||||
|
||||
@@ -143,10 +143,6 @@ pub async fn init_app_state(args: InitArgs) -> Result<(bool, AppState), InitErro
|
||||
|
||||
let object_store = build_objectstore(&args.data_dir, config.server.s3_storage_config.as_ref())?;
|
||||
|
||||
// Write out the latest .js/.d.ts runtime files.
|
||||
#[cfg(feature = "v8")]
|
||||
trailbase_js::runtime::write_js_runtime_files(args.data_dir.root()).await;
|
||||
|
||||
let app_state = AppState::new(AppStateArgs {
|
||||
data_dir: args.data_dir.clone(),
|
||||
public_url: args.public_url,
|
||||
|
||||
@@ -151,17 +151,6 @@ impl Server {
|
||||
|
||||
let mut custom_routers: Vec<Router<AppState>> = vec![];
|
||||
|
||||
#[cfg(feature = "v8")]
|
||||
if let Some(js_router) = crate::js::runtime::load_routes_and_jobs_from_js_modules(
|
||||
&state,
|
||||
state.data_dir().root().join("scripts"),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| InitError::ScriptError(err.to_string()))?
|
||||
{
|
||||
custom_routers.push(js_router);
|
||||
}
|
||||
|
||||
for rt in state.wasm_runtimes() {
|
||||
if let Some(wasm_router) = crate::wasm::install_routes_and_jobs(&state, rt.clone())
|
||||
.await
|
||||
@@ -643,11 +632,6 @@ async fn start_listen(
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "v8"))]
|
||||
tokio_rustls::rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("Failed to install TLS provider");
|
||||
|
||||
let server_config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(vec![cert], key)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
[package]
|
||||
name = "trailbase-js"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
license = "OSL-3.0"
|
||||
description = "JS runtime for the TrailBase framework"
|
||||
homepage = "https://trailbase.io"
|
||||
readme = "../README.md"
|
||||
exclude = [
|
||||
"**/node_modules/",
|
||||
"**/dist/",
|
||||
]
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
bytes = { version = "1.8.0", features = ["serde"] }
|
||||
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
|
||||
kanal = "0.1.1"
|
||||
log = { version = "^0.4.21", default-features = false }
|
||||
parking_lot = { workspace = true }
|
||||
rusqlite = { workspace = true }
|
||||
rust-embed = { workspace = true }
|
||||
rustyscript = { version = "^0.12.0", features = ["web", "fs"] }
|
||||
self_cell = "1.2.0"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
trailbase-schema = { workspace = true }
|
||||
trailbase-sqlite = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
trailbase-build = { workspace = true }
|
||||
1
crates/js-runtime/assets/runtime/.gitignore
vendored
1
crates/js-runtime/assets/runtime/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
dist/
|
||||
@@ -1,30 +0,0 @@
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
ignores: ["dist/", "node_modules/"],
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.{js,mjs,cjs,mts,ts,tsx,jsx}"],
|
||||
rules: {
|
||||
// https://typescript-eslint.io/rules/no-explicit-any/
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-wrapper-object-types": "warn",
|
||||
"@typescript-eslint/no-namespace": "off",
|
||||
"no-var": "off",
|
||||
// http://eslint.org/docs/rules/no-unused-vars
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
vars: "all",
|
||||
args: "after-used",
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "trailbase-js-runtime",
|
||||
"description": "Runtime for JS/TS execution within TrailBase",
|
||||
"version": "0.1.0",
|
||||
"license": "OSL-3.0",
|
||||
"type": "module",
|
||||
"homepage": "https://trailbase.io",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"check": "tsc --noEmit --skipLibCheck && eslint && vitest run",
|
||||
"format": "prettier -w src",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
@@ -1,808 +0,0 @@
|
||||
declare namespace Deno {
|
||||
export interface ReadFileOptions {
|
||||
/**
|
||||
* An abort signal to allow cancellation of the file read operation.
|
||||
* If the signal becomes aborted the readFile operation will be stopped
|
||||
* and the promise returned will be rejected with an AbortError.
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface WriteFileOptions {
|
||||
/** If set to `true`, will append to a file instead of overwriting previous
|
||||
* contents.
|
||||
*
|
||||
* @default {false} */
|
||||
append?: boolean;
|
||||
/** Sets the option to allow creating a new file, if one doesn't already
|
||||
* exist at the specified path.
|
||||
*
|
||||
* @default {true} */
|
||||
create?: boolean;
|
||||
/** If set to `true`, no file, directory, or symlink is allowed to exist at
|
||||
* the target location. When createNew is set to `true`, `create` is ignored.
|
||||
*
|
||||
* @default {false} */
|
||||
createNew?: boolean;
|
||||
/** Permissions always applied to file. */
|
||||
mode?: number;
|
||||
/** An abort signal to allow cancellation of the file write operation.
|
||||
*
|
||||
* If the signal becomes aborted the write file operation will be stopped
|
||||
* and the promise returned will be rejected with an {@linkcode AbortError}.
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options which can be set when using {@linkcode Deno.makeTempDir},
|
||||
* {@linkcode Deno.makeTempDirSync}, {@linkcode Deno.makeTempFile}, and
|
||||
* {@linkcode Deno.makeTempFileSync}.
|
||||
*
|
||||
* @category File System */
|
||||
export interface MakeTempOptions {
|
||||
/** Directory where the temporary directory should be created (defaults to
|
||||
* the env variable `TMPDIR`, or the system's default, usually `/tmp`).
|
||||
*
|
||||
* Note that if the passed `dir` is relative, the path returned by
|
||||
* `makeTempFile()` and `makeTempDir()` will also be relative. Be mindful of
|
||||
* this when changing working directory. */
|
||||
dir?: string;
|
||||
/** String that should precede the random portion of the temporary
|
||||
* directory's name. */
|
||||
prefix?: string;
|
||||
/** String that should follow the random portion of the temporary
|
||||
* directory's name. */
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options which can be set when using {@linkcode Deno.mkdir} and
|
||||
* {@linkcode Deno.mkdirSync}.
|
||||
*
|
||||
* @category File System */
|
||||
export interface MkdirOptions {
|
||||
/** If set to `true`, means that any intermediate directories will also be
|
||||
* created (as with the shell command `mkdir -p`).
|
||||
*
|
||||
* Intermediate directories are created with the same permissions.
|
||||
*
|
||||
* When recursive is set to `true`, succeeds silently (without changing any
|
||||
* permissions) if a directory already exists at the path, or if the path
|
||||
* is a symlink to an existing directory.
|
||||
*
|
||||
* @default {false} */
|
||||
recursive?: boolean;
|
||||
/** Permissions to use when creating the directory (defaults to `0o777`,
|
||||
* before the process's umask).
|
||||
*
|
||||
* Ignored on Windows. */
|
||||
mode?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about a directory entry returned from {@linkcode Deno.readDir}
|
||||
* and {@linkcode Deno.readDirSync}.
|
||||
*
|
||||
* @category File System */
|
||||
export interface DirEntry {
|
||||
/** The file name of the entry. It is just the entity name and does not
|
||||
* include the full path. */
|
||||
name: string;
|
||||
/** True if this is info for a regular file. Mutually exclusive to
|
||||
* `DirEntry.isDirectory` and `DirEntry.isSymlink`. */
|
||||
isFile: boolean;
|
||||
/** True if this is info for a regular directory. Mutually exclusive to
|
||||
* `DirEntry.isFile` and `DirEntry.isSymlink`. */
|
||||
isDirectory: boolean;
|
||||
/** True if this is info for a symlink. Mutually exclusive to
|
||||
* `DirEntry.isFile` and `DirEntry.isDirectory`. */
|
||||
isSymlink: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options which can be set when doing {@linkcode Deno.open} and
|
||||
* {@linkcode Deno.openSync}.
|
||||
*
|
||||
* @category File System */
|
||||
export interface OpenOptions {
|
||||
/** Sets the option for read access. This option, when `true`, means that
|
||||
* the file should be read-able if opened.
|
||||
*
|
||||
* @default {true} */
|
||||
read?: boolean;
|
||||
/** Sets the option for write access. This option, when `true`, means that
|
||||
* the file should be write-able if opened. If the file already exists,
|
||||
* any write calls on it will overwrite its contents, by default without
|
||||
* truncating it.
|
||||
*
|
||||
* @default {false} */
|
||||
write?: boolean;
|
||||
/** Sets the option for the append mode. This option, when `true`, means
|
||||
* that writes will append to a file instead of overwriting previous
|
||||
* contents.
|
||||
*
|
||||
* Note that setting `{ write: true, append: true }` has the same effect as
|
||||
* setting only `{ append: true }`.
|
||||
*
|
||||
* @default {false} */
|
||||
append?: boolean;
|
||||
/** Sets the option for truncating a previous file. If a file is
|
||||
* successfully opened with this option set it will truncate the file to `0`
|
||||
* size if it already exists. The file must be opened with write access
|
||||
* for truncate to work.
|
||||
*
|
||||
* @default {false} */
|
||||
truncate?: boolean;
|
||||
/** Sets the option to allow creating a new file, if one doesn't already
|
||||
* exist at the specified path. Requires write or append access to be
|
||||
* used.
|
||||
*
|
||||
* @default {false} */
|
||||
create?: boolean;
|
||||
/** If set to `true`, no file, directory, or symlink is allowed to exist at
|
||||
* the target location. Requires write or append access to be used. When
|
||||
* createNew is set to `true`, create and truncate are ignored.
|
||||
*
|
||||
* @default {false} */
|
||||
createNew?: boolean;
|
||||
/** Permissions to use if creating the file (defaults to `0o666`, before
|
||||
* the process's umask).
|
||||
*
|
||||
* Ignored on Windows. */
|
||||
mode?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options which can be set when using {@linkcode Deno.remove} and
|
||||
* {@linkcode Deno.removeSync}.
|
||||
*
|
||||
* @category File System */
|
||||
export interface RemoveOptions {
|
||||
/** If set to `true`, path will be removed even if it's a non-empty directory.
|
||||
*
|
||||
* @default {false} */
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
/** Options that can be used with {@linkcode symlink} and
|
||||
* {@linkcode symlinkSync}.
|
||||
*
|
||||
* @category File System */
|
||||
export interface SymlinkOptions {
|
||||
/** Specify the symbolic link type as file, directory or NTFS junction. This
|
||||
* option only applies to Windows and is ignored on other operating systems. */
|
||||
type: "file" | "dir" | "junction";
|
||||
}
|
||||
|
||||
export function writeFile(
|
||||
path: string | URL,
|
||||
data: Uint8Array | ReadableStream<Uint8Array>,
|
||||
options?: WriteFileOptions,
|
||||
): Promise<void>;
|
||||
|
||||
export function writeTextFile(
|
||||
path: string | URL,
|
||||
data: string | ReadableStream<string>,
|
||||
options?: WriteFileOptions,
|
||||
): Promise<void>;
|
||||
|
||||
export function readTextFile(
|
||||
path: string | URL,
|
||||
options?: ReadFileOptions,
|
||||
): Promise<string>;
|
||||
|
||||
export function readFile(
|
||||
path: string | URL,
|
||||
options?: ReadFileOptions,
|
||||
): Promise<Uint8Array>;
|
||||
|
||||
export function chmod(path: string | URL, mode: number): Promise<void>;
|
||||
|
||||
export function chown(
|
||||
path: string | URL,
|
||||
uid: number | null,
|
||||
gid: number | null,
|
||||
): Promise<void>;
|
||||
|
||||
export function cwd(): string;
|
||||
|
||||
export function makeTempDir(options?: MakeTempOptions): Promise<string>;
|
||||
|
||||
export function makeTempFile(options?: MakeTempOptions): Promise<string>;
|
||||
|
||||
export function mkdir(
|
||||
path: string | URL,
|
||||
options?: MkdirOptions,
|
||||
): Promise<void>;
|
||||
|
||||
export function chdir(directory: string | URL): void;
|
||||
|
||||
export function copyFile(
|
||||
fromPath: string | URL,
|
||||
toPath: string | URL,
|
||||
): Promise<void>;
|
||||
|
||||
export function readDir(path: string | URL): AsyncIterable<DirEntry>;
|
||||
|
||||
export function readLink(path: string | URL): Promise<string>;
|
||||
|
||||
export function realPath(path: string | URL): Promise<string>;
|
||||
|
||||
export function remove(
|
||||
path: string | URL,
|
||||
options?: RemoveOptions,
|
||||
): Promise<void>;
|
||||
|
||||
export function rename(
|
||||
oldpath: string | URL,
|
||||
newpath: string | URL,
|
||||
): Promise<void>;
|
||||
|
||||
export function stat(path: string | URL): Promise<FileInfo>;
|
||||
|
||||
export function lstat(path: string | URL): Promise<FileInfo>;
|
||||
|
||||
export function truncate(name: string, len?: number): Promise<void>;
|
||||
|
||||
export function open(
|
||||
path: string | URL,
|
||||
options?: OpenOptions,
|
||||
): Promise<FsFile>;
|
||||
|
||||
export function create(path: string | URL): Promise<FsFile>;
|
||||
|
||||
export function symlink(
|
||||
oldpath: string | URL,
|
||||
newpath: string | URL,
|
||||
options?: SymlinkOptions,
|
||||
): Promise<void>;
|
||||
|
||||
export function link(oldpath: string, newpath: string): Promise<void>;
|
||||
|
||||
export function utime(
|
||||
path: string | URL,
|
||||
atime: number | Date,
|
||||
mtime: number | Date,
|
||||
): Promise<void>;
|
||||
|
||||
export function umask(mask?: number): number;
|
||||
|
||||
/** Provides information about a file and is returned by
|
||||
* {@linkcode Deno.stat}, {@linkcode Deno.lstat}, {@linkcode Deno.statSync},
|
||||
* and {@linkcode Deno.lstatSync} or from calling `stat()` and `statSync()`
|
||||
* on an {@linkcode Deno.FsFile} instance.
|
||||
*
|
||||
* @category File System
|
||||
*/
|
||||
export interface FileInfo {
|
||||
/** True if this is info for a regular file. Mutually exclusive to
|
||||
* `FileInfo.isDirectory` and `FileInfo.isSymlink`. */
|
||||
isFile: boolean;
|
||||
/** True if this is info for a regular directory. Mutually exclusive to
|
||||
* `FileInfo.isFile` and `FileInfo.isSymlink`. */
|
||||
isDirectory: boolean;
|
||||
/** True if this is info for a symlink. Mutually exclusive to
|
||||
* `FileInfo.isFile` and `FileInfo.isDirectory`. */
|
||||
isSymlink: boolean;
|
||||
/** The size of the file, in bytes. */
|
||||
size: number;
|
||||
/** The last modification time of the file. This corresponds to the `mtime`
|
||||
* field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This
|
||||
* may not be available on all platforms. */
|
||||
mtime: Date | null;
|
||||
/** The last access time of the file. This corresponds to the `atime`
|
||||
* field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not
|
||||
* be available on all platforms. */
|
||||
atime: Date | null;
|
||||
/** The creation time of the file. This corresponds to the `birthtime`
|
||||
* field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may
|
||||
* not be available on all platforms. */
|
||||
birthtime: Date | null;
|
||||
/** The last change time of the file. This corresponds to the `ctime`
|
||||
* field from `stat` on Mac/BSD and `ChangeTime` on Windows. This may
|
||||
* not be available on all platforms. */
|
||||
ctime: Date | null;
|
||||
/** ID of the device containing the file. */
|
||||
dev: number;
|
||||
/** Inode number.
|
||||
*
|
||||
* _Linux/Mac OS only._ */
|
||||
ino: number | null;
|
||||
/** The underlying raw `st_mode` bits that contain the standard Unix
|
||||
* permissions for this file/directory.
|
||||
*/
|
||||
mode: number | null;
|
||||
/** Number of hard links pointing to this file.
|
||||
*
|
||||
* _Linux/Mac OS only._ */
|
||||
nlink: number | null;
|
||||
/** User ID of the owner of this file.
|
||||
*
|
||||
* _Linux/Mac OS only._ */
|
||||
uid: number | null;
|
||||
/** Group ID of the owner of this file.
|
||||
*
|
||||
* _Linux/Mac OS only._ */
|
||||
gid: number | null;
|
||||
/** Device ID of this file.
|
||||
*
|
||||
* _Linux/Mac OS only._ */
|
||||
rdev: number | null;
|
||||
/** Blocksize for filesystem I/O.
|
||||
*
|
||||
* _Linux/Mac OS only._ */
|
||||
blksize: number | null;
|
||||
/** Number of blocks allocated to the file, in 512-byte units.
|
||||
*
|
||||
* _Linux/Mac OS only._ */
|
||||
blocks: number | null;
|
||||
/** True if this is info for a block device.
|
||||
*
|
||||
* _Linux/Mac OS only._ */
|
||||
isBlockDevice: boolean | null;
|
||||
/** True if this is info for a char device.
|
||||
*
|
||||
* _Linux/Mac OS only._ */
|
||||
isCharDevice: boolean | null;
|
||||
/** True if this is info for a fifo.
|
||||
*
|
||||
* _Linux/Mac OS only._ */
|
||||
isFifo: boolean | null;
|
||||
/** True if this is info for a socket.
|
||||
*
|
||||
* _Linux/Mac OS only._ */
|
||||
isSocket: boolean | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A enum which defines the seek mode for IO related APIs that support
|
||||
* seeking.
|
||||
*
|
||||
* @category I/O */
|
||||
export enum SeekMode {
|
||||
/* Seek from the start of the file/resource. */
|
||||
Start = 0,
|
||||
/* Seek from the current position within the file/resource. */
|
||||
Current = 1,
|
||||
/* Seek from the end of the current file/resource. */
|
||||
End = 2,
|
||||
}
|
||||
|
||||
/** @category I/O */
|
||||
export interface SetRawOptions {
|
||||
/**
|
||||
* The `cbreak` option can be used to indicate that characters that
|
||||
* correspond to a signal should still be generated. When disabling raw
|
||||
* mode, this option is ignored. This functionality currently only works on
|
||||
* Linux and Mac OS.
|
||||
*/
|
||||
cbreak: boolean;
|
||||
}
|
||||
|
||||
export class FsFile implements Disposable {
|
||||
/** A {@linkcode ReadableStream} instance representing to the byte contents
|
||||
* of the file. This makes it easy to interoperate with other web streams
|
||||
* based APIs.
|
||||
*
|
||||
* ```ts
|
||||
* using file = await Deno.open("my_file.txt", { read: true });
|
||||
* const decoder = new TextDecoder();
|
||||
* for await (const chunk of file.readable) {
|
||||
* console.log(decoder.decode(chunk));
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
readonly readable: ReadableStream<Uint8Array>;
|
||||
/** A {@linkcode WritableStream} instance to write the contents of the
|
||||
* file. This makes it easy to interoperate with other web streams based
|
||||
* APIs.
|
||||
*
|
||||
* ```ts
|
||||
* const items = ["hello", "world"];
|
||||
* using file = await Deno.open("my_file.txt", { write: true });
|
||||
* const encoder = new TextEncoder();
|
||||
* const writer = file.writable.getWriter();
|
||||
* for (const item of items) {
|
||||
* await writer.write(encoder.encode(item));
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
readonly writable: WritableStream<Uint8Array>;
|
||||
/** Write the contents of the array buffer (`p`) to the file.
|
||||
*
|
||||
* Resolves to the number of bytes written.
|
||||
*
|
||||
* **It is not guaranteed that the full buffer will be written in a single
|
||||
* call.**
|
||||
*
|
||||
* ```ts
|
||||
* const encoder = new TextEncoder();
|
||||
* const data = encoder.encode("Hello world");
|
||||
* using file = await Deno.open("/foo/bar.txt", { write: true });
|
||||
* const bytesWritten = await file.write(data); // 11
|
||||
* ```
|
||||
*
|
||||
* @category I/O
|
||||
*/
|
||||
write(p: Uint8Array): Promise<number>;
|
||||
/** Synchronously write the contents of the array buffer (`p`) to the file.
|
||||
*
|
||||
* Returns the number of bytes written.
|
||||
*
|
||||
* **It is not guaranteed that the full buffer will be written in a single
|
||||
* call.**
|
||||
*
|
||||
* ```ts
|
||||
* const encoder = new TextEncoder();
|
||||
* const data = encoder.encode("Hello world");
|
||||
* using file = Deno.openSync("/foo/bar.txt", { write: true });
|
||||
* const bytesWritten = file.writeSync(data); // 11
|
||||
* ```
|
||||
*/
|
||||
writeSync(p: Uint8Array): number;
|
||||
/** Truncates (or extends) the file to reach the specified `len`. If `len`
|
||||
* is not specified, then the entire file contents are truncated.
|
||||
*
|
||||
* ### Truncate the entire file
|
||||
*
|
||||
* ```ts
|
||||
* using file = await Deno.open("my_file.txt", { write: true });
|
||||
* await file.truncate();
|
||||
* ```
|
||||
*
|
||||
* ### Truncate part of the file
|
||||
*
|
||||
* ```ts
|
||||
* // if "my_file.txt" contains the text "hello world":
|
||||
* using file = await Deno.open("my_file.txt", { write: true });
|
||||
* await file.truncate(7);
|
||||
* const buf = new Uint8Array(100);
|
||||
* await file.read(buf);
|
||||
* const text = new TextDecoder().decode(buf); // "hello w"
|
||||
* ```
|
||||
*/
|
||||
truncate(len?: number): Promise<void>;
|
||||
/** Synchronously truncates (or extends) the file to reach the specified
|
||||
* `len`. If `len` is not specified, then the entire file contents are
|
||||
* truncated.
|
||||
*
|
||||
* ### Truncate the entire file
|
||||
*
|
||||
* ```ts
|
||||
* using file = Deno.openSync("my_file.txt", { write: true });
|
||||
* file.truncateSync();
|
||||
* ```
|
||||
*
|
||||
* ### Truncate part of the file
|
||||
*
|
||||
* ```ts
|
||||
* // if "my_file.txt" contains the text "hello world":
|
||||
* using file = Deno.openSync("my_file.txt", { write: true });
|
||||
* file.truncateSync(7);
|
||||
* const buf = new Uint8Array(100);
|
||||
* file.readSync(buf);
|
||||
* const text = new TextDecoder().decode(buf); // "hello w"
|
||||
* ```
|
||||
*/
|
||||
truncateSync(len?: number): void;
|
||||
/** Read the file into an array buffer (`p`).
|
||||
*
|
||||
* Resolves to either the number of bytes read during the operation or EOF
|
||||
* (`null`) if there was nothing more to read.
|
||||
*
|
||||
* It is possible for a read to successfully return with `0` bytes. This
|
||||
* does not indicate EOF.
|
||||
*
|
||||
* **It is not guaranteed that the full buffer will be read in a single
|
||||
* call.**
|
||||
*
|
||||
* ```ts
|
||||
* // if "/foo/bar.txt" contains the text "hello world":
|
||||
* using file = await Deno.open("/foo/bar.txt");
|
||||
* const buf = new Uint8Array(100);
|
||||
* const numberOfBytesRead = await file.read(buf); // 11 bytes
|
||||
* const text = new TextDecoder().decode(buf); // "hello world"
|
||||
* ```
|
||||
*/
|
||||
read(p: Uint8Array): Promise<number | null>;
|
||||
/** Synchronously read from the file into an array buffer (`p`).
|
||||
*
|
||||
* Returns either the number of bytes read during the operation or EOF
|
||||
* (`null`) if there was nothing more to read.
|
||||
*
|
||||
* It is possible for a read to successfully return with `0` bytes. This
|
||||
* does not indicate EOF.
|
||||
*
|
||||
* **It is not guaranteed that the full buffer will be read in a single
|
||||
* call.**
|
||||
*
|
||||
* ```ts
|
||||
* // if "/foo/bar.txt" contains the text "hello world":
|
||||
* using file = Deno.openSync("/foo/bar.txt");
|
||||
* const buf = new Uint8Array(100);
|
||||
* const numberOfBytesRead = file.readSync(buf); // 11 bytes
|
||||
* const text = new TextDecoder().decode(buf); // "hello world"
|
||||
* ```
|
||||
*/
|
||||
readSync(p: Uint8Array): number | null;
|
||||
/** Seek to the given `offset` under mode given by `whence`. The call
|
||||
* resolves to the new position within the resource (bytes from the start).
|
||||
*
|
||||
* ```ts
|
||||
* // Given the file contains "Hello world" text, which is 11 bytes long:
|
||||
* using file = await Deno.open(
|
||||
* "hello.txt",
|
||||
* { read: true, write: true, truncate: true, create: true },
|
||||
* );
|
||||
* await file.write(new TextEncoder().encode("Hello world"));
|
||||
*
|
||||
* // advance cursor 6 bytes
|
||||
* const cursorPosition = await file.seek(6, Deno.SeekMode.Start);
|
||||
* console.log(cursorPosition); // 6
|
||||
* const buf = new Uint8Array(100);
|
||||
* await file.read(buf);
|
||||
* console.log(new TextDecoder().decode(buf)); // "world"
|
||||
* ```
|
||||
*
|
||||
* The seek modes work as follows:
|
||||
*
|
||||
* ```ts
|
||||
* // Given the file contains "Hello world" text, which is 11 bytes long:
|
||||
* const file = await Deno.open(
|
||||
* "hello.txt",
|
||||
* { read: true, write: true, truncate: true, create: true },
|
||||
* );
|
||||
* await file.write(new TextEncoder().encode("Hello world"));
|
||||
*
|
||||
* // Seek 6 bytes from the start of the file
|
||||
* console.log(await file.seek(6, Deno.SeekMode.Start)); // "6"
|
||||
* // Seek 2 more bytes from the current position
|
||||
* console.log(await file.seek(2, Deno.SeekMode.Current)); // "8"
|
||||
* // Seek backwards 2 bytes from the end of the file
|
||||
* console.log(await file.seek(-2, Deno.SeekMode.End)); // "9" (i.e. 11-2)
|
||||
* ```
|
||||
*/
|
||||
seek(offset: number | bigint, whence: SeekMode): Promise<number>;
|
||||
/** Synchronously seek to the given `offset` under mode given by `whence`.
|
||||
* The new position within the resource (bytes from the start) is returned.
|
||||
*
|
||||
* ```ts
|
||||
* using file = Deno.openSync(
|
||||
* "hello.txt",
|
||||
* { read: true, write: true, truncate: true, create: true },
|
||||
* );
|
||||
* file.writeSync(new TextEncoder().encode("Hello world"));
|
||||
*
|
||||
* // advance cursor 6 bytes
|
||||
* const cursorPosition = file.seekSync(6, Deno.SeekMode.Start);
|
||||
* console.log(cursorPosition); // 6
|
||||
* const buf = new Uint8Array(100);
|
||||
* file.readSync(buf);
|
||||
* console.log(new TextDecoder().decode(buf)); // "world"
|
||||
* ```
|
||||
*
|
||||
* The seek modes work as follows:
|
||||
*
|
||||
* ```ts
|
||||
* // Given the file contains "Hello world" text, which is 11 bytes long:
|
||||
* using file = Deno.openSync(
|
||||
* "hello.txt",
|
||||
* { read: true, write: true, truncate: true, create: true },
|
||||
* );
|
||||
* file.writeSync(new TextEncoder().encode("Hello world"));
|
||||
*
|
||||
* // Seek 6 bytes from the start of the file
|
||||
* console.log(file.seekSync(6, Deno.SeekMode.Start)); // "6"
|
||||
* // Seek 2 more bytes from the current position
|
||||
* console.log(file.seekSync(2, Deno.SeekMode.Current)); // "8"
|
||||
* // Seek backwards 2 bytes from the end of the file
|
||||
* console.log(file.seekSync(-2, Deno.SeekMode.End)); // "9" (i.e. 11-2)
|
||||
* ```
|
||||
*/
|
||||
seekSync(offset: number | bigint, whence: SeekMode): number;
|
||||
/** Resolves to a {@linkcode Deno.FileInfo} for the file.
|
||||
*
|
||||
* ```ts
|
||||
* import { assert } from "jsr:@std/assert";
|
||||
*
|
||||
* using file = await Deno.open("hello.txt");
|
||||
* const fileInfo = await file.stat();
|
||||
* assert(fileInfo.isFile);
|
||||
* ```
|
||||
*/
|
||||
stat(): Promise<FileInfo>;
|
||||
/** Synchronously returns a {@linkcode Deno.FileInfo} for the file.
|
||||
*
|
||||
* ```ts
|
||||
* import { assert } from "jsr:@std/assert";
|
||||
*
|
||||
* using file = Deno.openSync("hello.txt")
|
||||
* const fileInfo = file.statSync();
|
||||
* assert(fileInfo.isFile);
|
||||
* ```
|
||||
*/
|
||||
statSync(): FileInfo;
|
||||
/**
|
||||
* Flushes any pending data and metadata operations of the given file
|
||||
* stream to disk.
|
||||
*
|
||||
* ```ts
|
||||
* const file = await Deno.open(
|
||||
* "my_file.txt",
|
||||
* { read: true, write: true, create: true },
|
||||
* );
|
||||
* await file.write(new TextEncoder().encode("Hello World"));
|
||||
* await file.truncate(1);
|
||||
* await file.sync();
|
||||
* console.log(await Deno.readTextFile("my_file.txt")); // H
|
||||
* ```
|
||||
*
|
||||
* @category I/O
|
||||
*/
|
||||
sync(): Promise<void>;
|
||||
/**
|
||||
* Synchronously flushes any pending data and metadata operations of the given
|
||||
* file stream to disk.
|
||||
*
|
||||
* ```ts
|
||||
* const file = Deno.openSync(
|
||||
* "my_file.txt",
|
||||
* { read: true, write: true, create: true },
|
||||
* );
|
||||
* file.writeSync(new TextEncoder().encode("Hello World"));
|
||||
* file.truncateSync(1);
|
||||
* file.syncSync();
|
||||
* console.log(Deno.readTextFileSync("my_file.txt")); // H
|
||||
* ```
|
||||
*
|
||||
* @category I/O
|
||||
*/
|
||||
syncSync(): void;
|
||||
/**
|
||||
* Flushes any pending data operations of the given file stream to disk.
|
||||
* ```ts
|
||||
* using file = await Deno.open(
|
||||
* "my_file.txt",
|
||||
* { read: true, write: true, create: true },
|
||||
* );
|
||||
* await file.write(new TextEncoder().encode("Hello World"));
|
||||
* await file.syncData();
|
||||
* console.log(await Deno.readTextFile("my_file.txt")); // Hello World
|
||||
* ```
|
||||
*
|
||||
* @category I/O
|
||||
*/
|
||||
syncData(): Promise<void>;
|
||||
/**
|
||||
* Synchronously flushes any pending data operations of the given file stream
|
||||
* to disk.
|
||||
*
|
||||
* ```ts
|
||||
* using file = Deno.openSync(
|
||||
* "my_file.txt",
|
||||
* { read: true, write: true, create: true },
|
||||
* );
|
||||
* file.writeSync(new TextEncoder().encode("Hello World"));
|
||||
* file.syncDataSync();
|
||||
* console.log(Deno.readTextFileSync("my_file.txt")); // Hello World
|
||||
* ```
|
||||
*
|
||||
* @category I/O
|
||||
*/
|
||||
syncDataSync(): void;
|
||||
/**
|
||||
* Changes the access (`atime`) and modification (`mtime`) times of the
|
||||
* file stream resource. Given times are either in seconds (UNIX epoch
|
||||
* time) or as `Date` objects.
|
||||
*
|
||||
* ```ts
|
||||
* using file = await Deno.open("file.txt", { create: true, write: true });
|
||||
* await file.utime(1556495550, new Date());
|
||||
* ```
|
||||
*
|
||||
* @category File System
|
||||
*/
|
||||
utime(atime: number | Date, mtime: number | Date): Promise<void>;
|
||||
/**
|
||||
* Synchronously changes the access (`atime`) and modification (`mtime`)
|
||||
* times of the file stream resource. Given times are either in seconds
|
||||
* (UNIX epoch time) or as `Date` objects.
|
||||
*
|
||||
* ```ts
|
||||
* using file = Deno.openSync("file.txt", { create: true, write: true });
|
||||
* file.utime(1556495550, new Date());
|
||||
* ```
|
||||
*
|
||||
* @category File System
|
||||
*/
|
||||
utimeSync(atime: number | Date, mtime: number | Date): void;
|
||||
/** **UNSTABLE**: New API, yet to be vetted.
|
||||
*
|
||||
* Checks if the file resource is a TTY (terminal).
|
||||
*
|
||||
* ```ts
|
||||
* // This example is system and context specific
|
||||
* using file = await Deno.open("/dev/tty6");
|
||||
* file.isTerminal(); // true
|
||||
* ```
|
||||
*/
|
||||
isTerminal(): boolean;
|
||||
/** **UNSTABLE**: New API, yet to be vetted.
|
||||
*
|
||||
* Set TTY to be under raw mode or not. In raw mode, characters are read and
|
||||
* returned as is, without being processed. All special processing of
|
||||
* characters by the terminal is disabled, including echoing input
|
||||
* characters. Reading from a TTY device in raw mode is faster than reading
|
||||
* from a TTY device in canonical mode.
|
||||
*
|
||||
* ```ts
|
||||
* using file = await Deno.open("/dev/tty6");
|
||||
* file.setRaw(true, { cbreak: true });
|
||||
* ```
|
||||
*/
|
||||
setRaw(mode: boolean, options?: SetRawOptions): void;
|
||||
/**
|
||||
* Acquire an advisory file-system lock for the file.
|
||||
*
|
||||
* @param [exclusive=false]
|
||||
*/
|
||||
lock(exclusive?: boolean): Promise<void>;
|
||||
/**
|
||||
* Synchronously acquire an advisory file-system lock synchronously for the file.
|
||||
*
|
||||
* @param [exclusive=false]
|
||||
*/
|
||||
lockSync(exclusive?: boolean): void;
|
||||
/**
|
||||
* Release an advisory file-system lock for the file.
|
||||
*/
|
||||
unlock(): Promise<void>;
|
||||
/**
|
||||
* Synchronously release an advisory file-system lock for the file.
|
||||
*/
|
||||
unlockSync(): void;
|
||||
/** Close the file. Closing a file when you are finished with it is
|
||||
* important to avoid leaking resources.
|
||||
*
|
||||
* ```ts
|
||||
* using file = await Deno.open("my_file.txt");
|
||||
* // do work with "file" object
|
||||
* ```
|
||||
*/
|
||||
close(): void;
|
||||
|
||||
[Symbol.dispose](): void;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Ideally we'd pull in Deno types from https://github.com/denoland/deno/blob/main/cli/tsc/dts/lib.deno.ns.d.ts but haven't found a good way.
|
||||
export namespace fs {
|
||||
export const writeFile = Deno.writeFile;
|
||||
export const writeTextFile = Deno.writeTextFile;
|
||||
export const readTextFile = Deno.readTextFile;
|
||||
export const readFile = Deno.readFile;
|
||||
export const chmod = Deno.chmod;
|
||||
export const chown = Deno.chown;
|
||||
export const cwd = Deno.cwd;
|
||||
export const makeTempDir = Deno.makeTempDir;
|
||||
export const makeTempFile = Deno.makeTempFile;
|
||||
export const mkdir = Deno.mkdir;
|
||||
export const chdir = Deno.chdir;
|
||||
export const copyFile = Deno.copyFile;
|
||||
export const readDir = Deno.readDir;
|
||||
export const readLink = Deno.readLink;
|
||||
export const realPath = Deno.realPath;
|
||||
export const remove = Deno.remove;
|
||||
export const rename = Deno.rename;
|
||||
export const stat = Deno.stat;
|
||||
export const lstat = Deno.lstat;
|
||||
export const truncate = Deno.truncate;
|
||||
export const FsFile = Deno.FsFile;
|
||||
export const open = Deno.open;
|
||||
export const create = Deno.create;
|
||||
export const symlink = Deno.symlink;
|
||||
export const link = Deno.link;
|
||||
export const utime = Deno.utime;
|
||||
export const umask = Deno.umask;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
export { fs } from "./deno";
|
||||
|
||||
// Redirect console output to stderr, to keep stdout for request logs.
|
||||
declare global {
|
||||
var Deno: {
|
||||
core: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
print: any;
|
||||
};
|
||||
};
|
||||
}
|
||||
const _logStderr = function (...args: unknown[]) {
|
||||
globalThis.Deno.core.print(`${args.join(" ")}\n`, /* to stderr = */ true);
|
||||
};
|
||||
globalThis.console.log = _logStderr;
|
||||
globalThis.console.info = _logStderr;
|
||||
globalThis.console.debug = _logStderr;
|
||||
|
||||
export {
|
||||
HttpError,
|
||||
StatusCodes,
|
||||
addCronCallback,
|
||||
addPeriodicCallback,
|
||||
addRoute,
|
||||
execute,
|
||||
htmlHandler,
|
||||
jsonHandler,
|
||||
parsePath,
|
||||
query,
|
||||
stringHandler,
|
||||
transaction,
|
||||
Transaction,
|
||||
} from "./trailbase";
|
||||
|
||||
export type {
|
||||
Blob,
|
||||
CallbackType,
|
||||
HeaderMapType,
|
||||
HtmlResponseType,
|
||||
JsonRequestType,
|
||||
JsonResponseType,
|
||||
MaybeResponse,
|
||||
Method,
|
||||
ParsedPath,
|
||||
PathParamsType,
|
||||
RequestType,
|
||||
ResponseType,
|
||||
StringRequestType,
|
||||
StringResponseType,
|
||||
UserType,
|
||||
} from "./trailbase";
|
||||
@@ -1,791 +0,0 @@
|
||||
import { decodeFallback, encodeFallback } from "./util";
|
||||
|
||||
declare global {
|
||||
function __dispatch(
|
||||
m: Method,
|
||||
route: string,
|
||||
uri: string,
|
||||
path: [string, string][],
|
||||
headers: [string, string][],
|
||||
user: UserType | undefined,
|
||||
body: Uint8Array,
|
||||
): Promise<ResponseType>;
|
||||
|
||||
function __dispatchCron(id: number): Promise<string | undefined>;
|
||||
|
||||
var rustyscript: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
functions: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async_functions: any;
|
||||
};
|
||||
}
|
||||
|
||||
export type HeaderMapType = { [key: string]: string };
|
||||
export type PathParamsType = { [key: string]: string };
|
||||
export type UserType = {
|
||||
/// Base64 encoded UUIDv7 user id.
|
||||
id: string;
|
||||
/// The user's email address.
|
||||
email: string;
|
||||
/// The user's CSRF token.
|
||||
csrf: string;
|
||||
};
|
||||
export type RequestType = {
|
||||
uri: string;
|
||||
params: PathParamsType;
|
||||
headers: HeaderMapType;
|
||||
user?: UserType;
|
||||
body?: Uint8Array;
|
||||
};
|
||||
export type ResponseType = {
|
||||
headers?: [string, string][];
|
||||
status?: number;
|
||||
body?: Uint8Array;
|
||||
};
|
||||
export type MaybeResponse<T> = Promise<T | undefined> | T | undefined;
|
||||
export type CallbackType = (req: RequestType) => MaybeResponse<ResponseType>;
|
||||
export type Method =
|
||||
| "DELETE"
|
||||
| "GET"
|
||||
| "HEAD"
|
||||
| "OPTIONS"
|
||||
| "PATCH"
|
||||
| "POST"
|
||||
| "PUT"
|
||||
| "TRACE";
|
||||
|
||||
/// HTTP status codes.
|
||||
///
|
||||
// source: https://github.com/prettymuchbryce/http-status-codes/blob/master/src/status-codes.ts
|
||||
export enum StatusCodes {
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.2.1
|
||||
///
|
||||
/// This interim response indicates that everything so far is OK and that the
|
||||
/// client should continue with the request or ignore it if it is already
|
||||
/// finished.
|
||||
CONTINUE = 100,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.2.2
|
||||
///
|
||||
/// This code is sent in response to an Upgrade request header by the client,
|
||||
/// and indicates the protocol the server is switching too.
|
||||
SWITCHING_PROTOCOLS = 101,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.1
|
||||
///
|
||||
/// This code indicates that the server has received and is processing the
|
||||
/// request, but no response is available yet.
|
||||
PROCESSING = 102,
|
||||
/// Official Documentation @ https://www.rfc-editor.org/rfc/rfc8297#page-3
|
||||
///
|
||||
/// This code indicates to the client that the server is likely to send a
|
||||
/// final response with the header fields included in the informational
|
||||
/// response.
|
||||
EARLY_HINTS = 103,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.1
|
||||
///
|
||||
/// The request has succeeded. The meaning of a success varies depending on the HTTP method:
|
||||
/// GET: The resource has been fetched and is transmitted in the message body.
|
||||
/// HEAD: The entity headers are in the message body.
|
||||
/// POST: The resource describing the result of the action is transmitted in the message body.
|
||||
/// TRACE: The message body contains the request message as received by the server
|
||||
OK = 200,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.2
|
||||
///
|
||||
/// The request has succeeded and a new resource has been created as a result
|
||||
/// of it. This is typically the response sent after a PUT request.
|
||||
CREATED = 201,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.3
|
||||
///
|
||||
/// The request has been received but not yet acted upon. It is
|
||||
/// non-committal, meaning that there is no way in HTTP to later send an
|
||||
/// asynchronous response indicating the outcome of processing the request. It
|
||||
/// is intended for cases where another process or server handles the request,
|
||||
/// or for batch processing.
|
||||
ACCEPTED = 202,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.4
|
||||
///
|
||||
/// This response code means returned meta-information set is not exact set
|
||||
/// as available from the origin server, but collected from a local or a third
|
||||
/// party copy. Except this condition, 200 OK response should be preferred
|
||||
/// instead of this response.
|
||||
NON_AUTHORITATIVE_INFORMATION = 203,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.5
|
||||
///
|
||||
/// There is no content to send for this request, but the headers may be
|
||||
/// useful. The user-agent may update its cached headers for this resource with
|
||||
/// the new ones.
|
||||
NO_CONTENT = 204,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.6
|
||||
///
|
||||
/// This response code is sent after accomplishing request to tell user agent
|
||||
/// reset document view which sent this request.
|
||||
RESET_CONTENT = 205,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7233#section-4.1
|
||||
///
|
||||
/// This response code is used because of range header sent by the client to
|
||||
/// separate download into multiple streams.
|
||||
PARTIAL_CONTENT = 206,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.2
|
||||
///
|
||||
/// A Multi-Status response conveys information about multiple resources in
|
||||
/// situations where multiple status codes might be appropriate.
|
||||
MULTI_STATUS = 207,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.1
|
||||
///
|
||||
/// The request has more than one possible responses. User-agent or user
|
||||
/// should choose one of them. There is no standardized way to choose one of
|
||||
/// the responses.
|
||||
MULTIPLE_CHOICES = 300,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.2
|
||||
///
|
||||
/// This response code means that URI of requested resource has been changed.
|
||||
/// Probably, new URI would be given in the response.
|
||||
MOVED_PERMANENTLY = 301,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.3
|
||||
///
|
||||
/// This response code means that URI of requested resource has been changed
|
||||
/// temporarily. New changes in the URI might be made in the future. Therefore,
|
||||
/// this same URI should be used by the client in future requests.
|
||||
MOVED_TEMPORARILY = 302,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.4
|
||||
///
|
||||
/// Server sent this response to directing client to get requested resource
|
||||
/// to another URI with an GET request.
|
||||
SEE_OTHER = 303,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7232#section-4.1
|
||||
///
|
||||
/// This is used for caching purposes. It is telling to client that response
|
||||
/// has not been modified. So, client can continue to use same cached version
|
||||
/// of response.
|
||||
NOT_MODIFIED = 304,
|
||||
/// @deprecated
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.6
|
||||
///
|
||||
/// Was defined in a previous version of the HTTP specification to indicate
|
||||
/// that a requested response must be accessed by a proxy. It has been
|
||||
/// deprecated due to security concerns regarding in-band configuration of a
|
||||
/// proxy.
|
||||
USE_PROXY = 305,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.7
|
||||
///
|
||||
/// Server sent this response to directing client to get requested resource
|
||||
/// to another URI with same method that used prior request. This has the same
|
||||
/// semantic than the 302 Found HTTP response code, with the exception that the
|
||||
/// user agent must not change the HTTP method used: if a POST was used in the
|
||||
/// first request, a POST must be used in the second request.
|
||||
TEMPORARY_REDIRECT = 307,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7538#section-3
|
||||
///
|
||||
/// This means that the resource is now permanently located at another URI,
|
||||
/// specified by the Location: HTTP Response header. This has the same
|
||||
/// semantics as the 301 Moved Permanently HTTP response code, with the
|
||||
/// exception that the user agent must not change the HTTP method used: if a
|
||||
/// POST was used in the first request, a POST must be used in the second
|
||||
/// request.
|
||||
PERMANENT_REDIRECT = 308,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.1
|
||||
///
|
||||
/// This response means that server could not understand the request due to invalid syntax.
|
||||
BAD_REQUEST = 400,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7235#section-3.1
|
||||
///
|
||||
/// Although the HTTP standard specifies "unauthorized", semantically this
|
||||
/// response means "unauthenticated". That is, the client must authenticate
|
||||
/// itself to get the requested response.
|
||||
UNAUTHORIZED = 401,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.2
|
||||
///
|
||||
/// This response code is reserved for future use. Initial aim for creating
|
||||
/// this code was using it for digital payment systems however this is not used
|
||||
/// currently.
|
||||
PAYMENT_REQUIRED = 402,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.3
|
||||
///
|
||||
/// The client does not have access rights to the content, i.e. they are
|
||||
/// unauthorized, so server is rejecting to give proper response. Unlike 401,
|
||||
/// the client's identity is known to the server.
|
||||
FORBIDDEN = 403,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.4
|
||||
///
|
||||
/// The server can not find requested resource. In the browser, this means
|
||||
/// the URL is not recognized. In an API, this can also mean that the endpoint
|
||||
/// is valid but the resource itself does not exist. Servers may also send this
|
||||
/// response instead of 403 to hide the existence of a resource from an
|
||||
/// unauthorized client. This response code is probably the most famous one due
|
||||
/// to its frequent occurence on the web.
|
||||
NOT_FOUND = 404,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.5
|
||||
///
|
||||
/// The request method is known by the server but has been disabled and
|
||||
/// cannot be used. For example, an API may forbid DELETE-ing a resource. The
|
||||
/// two mandatory methods, GET and HEAD, must never be disabled and should not
|
||||
/// return this error code.
|
||||
METHOD_NOT_ALLOWED = 405,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.6
|
||||
///
|
||||
/// This response is sent when the web server, after performing server-driven
|
||||
/// content negotiation, doesn't find any content following the criteria given
|
||||
/// by the user agent.
|
||||
NOT_ACCEPTABLE = 406,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7235#section-3.2
|
||||
///
|
||||
/// This is similar to 401 but authentication is needed to be done by a proxy.
|
||||
PROXY_AUTHENTICATION_REQUIRED = 407,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.7
|
||||
///
|
||||
/// This response is sent on an idle connection by some servers, even without
|
||||
/// any previous request by the client. It means that the server would like to
|
||||
/// shut down this unused connection. This response is used much more since
|
||||
/// some browsers, like Chrome, Firefox 27+, or IE9, use HTTP pre-connection
|
||||
/// mechanisms to speed up surfing. Also note that some servers merely shut
|
||||
/// down the connection without sending this message.
|
||||
REQUEST_TIMEOUT = 408,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.8
|
||||
///
|
||||
/// This response is sent when a request conflicts with the current state of the server.
|
||||
CONFLICT = 409,
|
||||
///
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.9
|
||||
///
|
||||
/// This response would be sent when the requested content has been
|
||||
/// permenantly deleted from server, with no forwarding address. Clients are
|
||||
/// expected to remove their caches and links to the resource. The HTTP
|
||||
/// specification intends this status code to be used for "limited-time,
|
||||
/// promotional services". APIs should not feel compelled to indicate resources
|
||||
/// that have been deleted with this status code.
|
||||
GONE = 410,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.10
|
||||
///
|
||||
/// The server rejected the request because the Content-Length header field
|
||||
/// is not defined and the server requires it.
|
||||
LENGTH_REQUIRED = 411,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7232#section-4.2
|
||||
///
|
||||
/// The client has indicated preconditions in its headers which the server
|
||||
/// does not meet.
|
||||
PRECONDITION_FAILED = 412,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.11
|
||||
///
|
||||
/// Request entity is larger than limits defined by server; the server might
|
||||
/// close the connection or return an Retry-After header field.
|
||||
REQUEST_TOO_LONG = 413,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.12
|
||||
///
|
||||
/// The URI requested by the client is longer than the server is willing to interpret.
|
||||
REQUEST_URI_TOO_LONG = 414,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.13
|
||||
///
|
||||
/// The media format of the requested data is not supported by the server, so
|
||||
/// the server is rejecting the request.
|
||||
UNSUPPORTED_MEDIA_TYPE = 415,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7233#section-4.4
|
||||
///
|
||||
/// The range specified by the Range header field in the request can't be
|
||||
/// fulfilled; it's possible that the range is outside the size of the target
|
||||
/// URI's data.
|
||||
REQUESTED_RANGE_NOT_SATISFIABLE = 416,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.14
|
||||
///
|
||||
/// This response code means the expectation indicated by the Expect request
|
||||
/// header field can't be met by the server.
|
||||
EXPECTATION_FAILED = 417,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc2324#section-2.3.2
|
||||
///
|
||||
/// Any attempt to brew coffee with a teapot should result in the error code
|
||||
/// "418 I'm a teapot". The resulting entity body MAY be short and stout.
|
||||
IM_A_TEAPOT = 418,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.6
|
||||
///
|
||||
/// The 507 (Insufficient Storage) status code means the method could not be
|
||||
/// performed on the resource because the server is unable to store the
|
||||
/// representation needed to successfully complete the request. This condition
|
||||
/// is considered to be temporary. If the request which received this status
|
||||
/// code was the result of a user action, the request MUST NOT be repeated
|
||||
/// until it is requested by a separate user action.
|
||||
INSUFFICIENT_SPACE_ON_RESOURCE = 419,
|
||||
/// @deprecated
|
||||
/// Official Documentation @ https://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt
|
||||
///
|
||||
/// A deprecated response used by the Spring Framework when a method has failed.
|
||||
METHOD_FAILURE = 420,
|
||||
/// Official Documentation @ https://datatracker.ietf.org/doc/html/rfc7540#section-9.1.2
|
||||
///
|
||||
/// Defined in the specification of HTTP/2 to indicate that a server is not
|
||||
/// able to produce a response for the combination of scheme and authority that
|
||||
/// are included in the request URI.
|
||||
MISDIRECTED_REQUEST = 421,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.3
|
||||
///
|
||||
/// The request was well-formed but was unable to be followed due to semantic errors.
|
||||
UNPROCESSABLE_ENTITY = 422,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.4
|
||||
///
|
||||
/// The resource that is being accessed is locked.
|
||||
LOCKED = 423,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.5
|
||||
///
|
||||
/// The request failed due to failure of a previous request.
|
||||
FAILED_DEPENDENCY = 424,
|
||||
/// Official Documentation @ https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.15
|
||||
///
|
||||
/// The server refuses to perform the request using the current protocol but
|
||||
/// might be willing to do so after the client upgrades to a different
|
||||
/// protocol.
|
||||
UPGRADE_REQUIRED = 426,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc6585#section-3
|
||||
///
|
||||
/// The origin server requires the request to be conditional. Intended to
|
||||
/// prevent the 'lost update' problem, where a client GETs a resource's state,
|
||||
/// modifies it, and PUTs it back to the server, when meanwhile a third party
|
||||
/// has modified the state on the server, leading to a conflict.
|
||||
PRECONDITION_REQUIRED = 428,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc6585#section-4
|
||||
///
|
||||
/// The user has sent too many requests in a given amount of time ("rate limiting").
|
||||
TOO_MANY_REQUESTS = 429,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc6585#section-5
|
||||
///
|
||||
/// The server is unwilling to process the request because its header fields
|
||||
/// are too large. The request MAY be resubmitted after reducing the size of
|
||||
/// the request header fields.
|
||||
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7725
|
||||
///
|
||||
/// The user-agent requested a resource that cannot legally be provided, such
|
||||
/// as a web page censored by a government.
|
||||
UNAVAILABLE_FOR_LEGAL_REASONS = 451,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.1
|
||||
///
|
||||
/// The server encountered an unexpected condition that prevented it from
|
||||
/// fulfilling the request.
|
||||
INTERNAL_SERVER_ERROR = 500,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.2
|
||||
///
|
||||
/// The request method is not supported by the server and cannot be handled.
|
||||
/// The only methods that servers are required to support (and therefore that
|
||||
/// must not return this code) are GET and HEAD.
|
||||
NOT_IMPLEMENTED = 501,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.3
|
||||
///
|
||||
/// This error response means that the server, while working as a gateway to
|
||||
/// get a response needed to handle the request, got an invalid response.
|
||||
BAD_GATEWAY = 502,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.4
|
||||
///
|
||||
/// The server is not ready to handle the request. Common causes are a server
|
||||
/// that is down for maintenance or that is overloaded. Note that together with
|
||||
/// this response, a user-friendly page explaining the problem should be sent.
|
||||
/// This responses should be used for temporary conditions and the Retry-After:
|
||||
/// HTTP header should, if possible, contain the estimated time before the
|
||||
/// recovery of the service. The webmaster must also take care about the
|
||||
/// caching-related headers that are sent along with this response, as these
|
||||
/// temporary condition responses should usually not be cached.
|
||||
SERVICE_UNAVAILABLE = 503,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.5
|
||||
///
|
||||
/// This error response is given when the server is acting as a gateway and
|
||||
/// cannot get a response in time.
|
||||
GATEWAY_TIMEOUT = 504,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.6
|
||||
///
|
||||
/// The HTTP version used in the request is not supported by the server.
|
||||
HTTP_VERSION_NOT_SUPPORTED = 505,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.6
|
||||
///
|
||||
/// The server has an internal configuration error: the chosen variant
|
||||
/// resource is configured to engage in transparent content negotiation itself,
|
||||
/// and is therefore not a proper end point in the negotiation process.
|
||||
INSUFFICIENT_STORAGE = 507,
|
||||
/// Official Documentation @ https://tools.ietf.org/html/rfc6585#section-6
|
||||
///
|
||||
/// The 511 status code indicates that the client needs to authenticate to
|
||||
/// gain network access.
|
||||
NETWORK_AUTHENTICATION_REQUIRED = 511,
|
||||
}
|
||||
|
||||
export class HttpError extends Error {
|
||||
readonly statusCode: number;
|
||||
readonly headers: [string, string][] | undefined;
|
||||
|
||||
constructor(
|
||||
statusCode: number,
|
||||
message?: string,
|
||||
headers?: [string, string][],
|
||||
) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
public override toString(): string {
|
||||
return `HttpError(${this.statusCode}, ${this.message})`;
|
||||
}
|
||||
|
||||
toResponse(): ResponseType {
|
||||
const m = this.message;
|
||||
return {
|
||||
headers: this.headers,
|
||||
status: this.statusCode,
|
||||
body: m !== "" ? encodeFallback(m) : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type StringRequestType = {
|
||||
uri: string;
|
||||
params: PathParamsType;
|
||||
headers: HeaderMapType;
|
||||
user?: UserType;
|
||||
body?: string;
|
||||
};
|
||||
export type StringResponseType = {
|
||||
headers?: [string, string][];
|
||||
status?: number;
|
||||
body: string;
|
||||
};
|
||||
|
||||
export function stringHandler(
|
||||
f: (req: StringRequestType) => MaybeResponse<StringResponseType | string>,
|
||||
): CallbackType {
|
||||
return async (req: RequestType): Promise<ResponseType | undefined> => {
|
||||
try {
|
||||
const body = req.body;
|
||||
const resp: StringResponseType | string | undefined = await f({
|
||||
uri: req.uri,
|
||||
params: req.params,
|
||||
headers: req.headers,
|
||||
user: req.user,
|
||||
body: body && decodeFallback(body),
|
||||
});
|
||||
|
||||
if (resp === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof resp === "string") {
|
||||
return {
|
||||
status: StatusCodes.OK,
|
||||
body: encodeFallback(resp),
|
||||
};
|
||||
}
|
||||
|
||||
const respBody = resp.body;
|
||||
return {
|
||||
headers: resp.headers,
|
||||
status: resp.status,
|
||||
body: respBody ? encodeFallback(respBody) : undefined,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return err.toResponse();
|
||||
}
|
||||
return {
|
||||
status: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
body: encodeFallback(`Uncaught error: ${err}`),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type HtmlResponseType = {
|
||||
headers?: [string, string][];
|
||||
status?: number;
|
||||
body: string;
|
||||
};
|
||||
|
||||
export function htmlHandler(
|
||||
f: (req: StringRequestType) => MaybeResponse<HtmlResponseType | string>,
|
||||
): CallbackType {
|
||||
return async (req: RequestType): Promise<ResponseType | undefined> => {
|
||||
try {
|
||||
const body = req.body;
|
||||
const resp: HtmlResponseType | string | undefined = await f({
|
||||
uri: req.uri,
|
||||
params: req.params,
|
||||
headers: req.headers,
|
||||
user: req.user,
|
||||
body: body && decodeFallback(body),
|
||||
});
|
||||
|
||||
if (resp === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof resp === "string") {
|
||||
return {
|
||||
headers: [["content-type", "text/html"]],
|
||||
status: StatusCodes.OK,
|
||||
body: encodeFallback(resp),
|
||||
};
|
||||
}
|
||||
|
||||
const respBody = resp.body;
|
||||
return {
|
||||
headers: [["content-type", "text/html"], ...(resp.headers ?? [])],
|
||||
status: resp.status,
|
||||
body: respBody ? encodeFallback(respBody) : undefined,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return err.toResponse();
|
||||
}
|
||||
return {
|
||||
status: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
body: encodeFallback(`Uncaught error: ${err}`),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type JsonRequestType = {
|
||||
uri: string;
|
||||
params: PathParamsType;
|
||||
headers: HeaderMapType;
|
||||
user?: UserType;
|
||||
body?: object | string;
|
||||
};
|
||||
export interface JsonResponseType {
|
||||
headers?: [string, string][];
|
||||
status?: number;
|
||||
body: object;
|
||||
}
|
||||
|
||||
export function jsonHandler(
|
||||
f: (req: JsonRequestType) => MaybeResponse<JsonRequestType | object>,
|
||||
): CallbackType {
|
||||
return async (req: RequestType): Promise<ResponseType | undefined> => {
|
||||
try {
|
||||
const body = req.body;
|
||||
const resp: JsonResponseType | object | undefined = await f({
|
||||
uri: req.uri,
|
||||
params: req.params,
|
||||
headers: req.headers,
|
||||
user: req.user,
|
||||
body: body && decodeFallback(body),
|
||||
});
|
||||
|
||||
if (resp === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ("body" in resp) {
|
||||
const r = resp as JsonResponseType;
|
||||
const rBody = r.body;
|
||||
return {
|
||||
headers: [["content-type", "application/json"], ...(r.headers ?? [])],
|
||||
status: r.status,
|
||||
body: rBody ? encodeFallback(JSON.stringify(rBody)) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
headers: [["content-type", "application/json"]],
|
||||
status: StatusCodes.OK,
|
||||
body: encodeFallback(JSON.stringify(resp)),
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return err.toResponse();
|
||||
}
|
||||
return {
|
||||
headers: [["content-type", "application/json"]],
|
||||
status: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
body: encodeFallback(`Uncaught error: ${err}`),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const routerCallbacks = new Map<string, CallbackType>();
|
||||
|
||||
function isolateId(): number {
|
||||
return rustyscript.functions.isolate_id();
|
||||
}
|
||||
|
||||
export function addRoute(
|
||||
method: Method,
|
||||
route: string,
|
||||
callback: CallbackType,
|
||||
) {
|
||||
if (isolateId() === 0) {
|
||||
rustyscript.functions.install_route(method, route);
|
||||
console.debug("JS: Added route:", method, route);
|
||||
}
|
||||
|
||||
routerCallbacks.set(`${method}:${route}`, callback);
|
||||
}
|
||||
|
||||
export async function dispatch(
|
||||
method: Method,
|
||||
route: string,
|
||||
uri: string,
|
||||
pathParams: [string, string][],
|
||||
headers: [string, string][],
|
||||
user: UserType | undefined,
|
||||
body: Uint8Array,
|
||||
): Promise<ResponseType> {
|
||||
const key = `${method}:${route}`;
|
||||
const cb: CallbackType | undefined = routerCallbacks.get(key);
|
||||
if (!cb) {
|
||||
throw Error(`Missing callback: ${key}`);
|
||||
}
|
||||
|
||||
return (
|
||||
(await cb({
|
||||
uri,
|
||||
params: Object.fromEntries(pathParams),
|
||||
headers: Object.fromEntries(headers),
|
||||
user: user,
|
||||
body,
|
||||
})) ?? { status: StatusCodes.OK }
|
||||
);
|
||||
}
|
||||
|
||||
globalThis.__dispatch = dispatch;
|
||||
|
||||
const cronCallbacks = new Map<number, () => void | Promise<void>>();
|
||||
|
||||
/// Installs a Cron job that is registered to be orchestrated from native code.
|
||||
export function addCronCallback(
|
||||
name: string,
|
||||
schedule: string,
|
||||
cb: () => void | Promise<void>,
|
||||
) {
|
||||
const cronRegex =
|
||||
/^(@(yearly|monthly|weekly|daily|hourly|))|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*)\s*){6,7})$/;
|
||||
|
||||
const matches = cronRegex.test(schedule);
|
||||
if (!matches) {
|
||||
throw Error(`Not a valid 6/7-component cron schedule: ${schedule}`);
|
||||
}
|
||||
|
||||
if (isolateId() === 0) {
|
||||
const id = rustyscript.functions.install_job(name, schedule);
|
||||
console.debug(`JS: Added cron job (id=${id}): "${name}"`);
|
||||
cronCallbacks.set(id, cb);
|
||||
}
|
||||
}
|
||||
|
||||
async function dispatchCron(id: number): Promise<string | undefined> {
|
||||
const cb: (() => void | Promise<void>) | undefined = cronCallbacks.get(id);
|
||||
if (!cb) {
|
||||
throw Error(`Missing cron callback: ${id}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await cb();
|
||||
} catch (err) {
|
||||
return `${err}`;
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.__dispatchCron = dispatchCron;
|
||||
|
||||
/// Installs a periodic callback in a single isolate and returns a cleanup function.
|
||||
export function addPeriodicCallback(
|
||||
milliseconds: number,
|
||||
cb: (cancel: () => void) => void,
|
||||
): () => void {
|
||||
// Note: right now we run periodic tasks only on the first isolate. This is
|
||||
// very simple but doesn't use other workers. This has nice properties in
|
||||
// terms of state management and hopefully work-stealing will alleviate the
|
||||
// issue, i.e. workers will pick up the slack in terms of incoming requests.
|
||||
if (isolateId() !== 0) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const handle = setInterval(() => {
|
||||
cb(() => clearInterval(handle));
|
||||
}, milliseconds);
|
||||
|
||||
return () => clearInterval(handle);
|
||||
}
|
||||
|
||||
export type Blob = {
|
||||
blob: string;
|
||||
};
|
||||
|
||||
export type SqlType = number | string | Blob | null;
|
||||
|
||||
/// Queries the SQLite database.
|
||||
export async function query(
|
||||
sql: string,
|
||||
params: SqlType[],
|
||||
): Promise<SqlType[][]> {
|
||||
return await rustyscript.async_functions.query(sql, params);
|
||||
}
|
||||
|
||||
/// Executes given query against the SQLite database.
|
||||
export async function execute(sql: string, params: SqlType[]): Promise<number> {
|
||||
return await rustyscript.async_functions.execute(sql, params);
|
||||
}
|
||||
|
||||
export class Transaction {
|
||||
finalized: boolean;
|
||||
|
||||
constructor() {
|
||||
this.finalized = false;
|
||||
}
|
||||
|
||||
public query(queryStr: string, params: SqlType[]): SqlType[][] {
|
||||
return rustyscript.functions.transaction_query(queryStr, params);
|
||||
}
|
||||
|
||||
public execute(queryStr: string, params: SqlType[]): SqlType {
|
||||
return rustyscript.functions.transaction_execute(queryStr, params);
|
||||
}
|
||||
|
||||
public commit(): void {
|
||||
this.finalized = true;
|
||||
rustyscript.functions.transaction_commit();
|
||||
}
|
||||
|
||||
public rollback(): void {
|
||||
this.finalized = true;
|
||||
rustyscript.functions.transaction_rollback();
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit a SQLite transaction.
|
||||
///
|
||||
/// NOTE: The API is async while the implementation is not. This is for
|
||||
/// future-proofing. This means that calling transaction() will block the
|
||||
/// event-loop until a write-lock on the underlying database connection can be
|
||||
/// acquired. In most scenarios this should be fine but may become a bottleneck
|
||||
/// when there's a lot of write congestion. In the future, we should update the
|
||||
/// implementation to be async.
|
||||
export async function transaction<T>(f: (tx: Transaction) => T): Promise<T> {
|
||||
await rustyscript.async_functions.transaction_begin();
|
||||
|
||||
const tx = new Transaction();
|
||||
try {
|
||||
const r = f(tx);
|
||||
if (!tx.finalized) {
|
||||
rustyscript.functions.transaction_rollback();
|
||||
}
|
||||
return r;
|
||||
} catch (e) {
|
||||
rustyscript.functions.transaction_rollback();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export type ParsedPath = {
|
||||
path: string;
|
||||
query: URLSearchParams;
|
||||
};
|
||||
|
||||
export function parsePath(path: string): ParsedPath {
|
||||
const queryIndex = path.indexOf("?");
|
||||
if (queryIndex >= 0) {
|
||||
return {
|
||||
path: path.slice(0, queryIndex),
|
||||
query: new URLSearchParams(path.slice(queryIndex + 1)),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
query: new URLSearchParams(),
|
||||
};
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
/// @param {Uint8Array} bytes
|
||||
/// @return {string}
|
||||
///
|
||||
/// source: https://github.com/samthor/fast-text-encoding
|
||||
export function decodeFallback(bytes: Uint8Array): string {
|
||||
var inputIndex = 0;
|
||||
|
||||
// Create a working buffer for UTF-16 code points, but don't generate one
|
||||
// which is too large for small input sizes. UTF-8 to UCS-16 conversion is
|
||||
// going to be at most 1:1, if all code points are ASCII. The other extreme
|
||||
// is 4-byte UTF-8, which results in two UCS-16 points, but this is still 50%
|
||||
// fewer entries in the output.
|
||||
var pendingSize = Math.min(256 * 256, bytes.length + 1);
|
||||
var pending = new Uint16Array(pendingSize);
|
||||
var chunks = [];
|
||||
var pendingIndex = 0;
|
||||
|
||||
for (;;) {
|
||||
var more = inputIndex < bytes.length;
|
||||
|
||||
// If there's no more data or there'd be no room for two UTF-16 values,
|
||||
// create a chunk. This isn't done at the end by simply slicing the data
|
||||
// into equal sized chunks as we might hit a surrogate pair.
|
||||
if (!more || pendingIndex >= pendingSize - 1) {
|
||||
// nb. .apply and friends are *really slow*. Low-hanging fruit is to
|
||||
// expand this to literally pass pending[0], pending[1], ... etc, but
|
||||
// the output code expands pretty fast in this case.
|
||||
// These extra vars get compiled out: they're just to make TS happy.
|
||||
// Turns out you can pass an ArrayLike to .apply().
|
||||
var subarray = pending.subarray(0, pendingIndex);
|
||||
var arraylike = subarray as unknown as number[];
|
||||
chunks.push(String.fromCharCode.apply(null, arraylike));
|
||||
|
||||
if (!more) {
|
||||
return chunks.join("");
|
||||
}
|
||||
|
||||
// Move the buffer forward and create another chunk.
|
||||
bytes = bytes.subarray(inputIndex);
|
||||
inputIndex = 0;
|
||||
pendingIndex = 0;
|
||||
}
|
||||
|
||||
// The native TextDecoder will generate "REPLACEMENT CHARACTER" where the
|
||||
// input data is invalid. Here, we blindly parse the data even if it's
|
||||
// wrong: e.g., if a 3-byte sequence doesn't have two valid continuations.
|
||||
|
||||
var byte1 = bytes[inputIndex++];
|
||||
if ((byte1 & 0x80) === 0) {
|
||||
// 1-byte or null
|
||||
pending[pendingIndex++] = byte1;
|
||||
} else if ((byte1 & 0xe0) === 0xc0) {
|
||||
// 2-byte
|
||||
var byte2 = bytes[inputIndex++] & 0x3f;
|
||||
pending[pendingIndex++] = ((byte1 & 0x1f) << 6) | byte2;
|
||||
} else if ((byte1 & 0xf0) === 0xe0) {
|
||||
// 3-byte
|
||||
var byte2 = bytes[inputIndex++] & 0x3f;
|
||||
var byte3 = bytes[inputIndex++] & 0x3f;
|
||||
pending[pendingIndex++] = ((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3;
|
||||
} else if ((byte1 & 0xf8) === 0xf0) {
|
||||
// 4-byte
|
||||
var byte2 = bytes[inputIndex++] & 0x3f;
|
||||
var byte3 = bytes[inputIndex++] & 0x3f;
|
||||
var byte4 = bytes[inputIndex++] & 0x3f;
|
||||
|
||||
// this can be > 0xffff, so possibly generate surrogates
|
||||
var codepoint =
|
||||
((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
|
||||
if (codepoint > 0xffff) {
|
||||
// codepoint &= ~0x10000;
|
||||
codepoint -= 0x10000;
|
||||
pending[pendingIndex++] = ((codepoint >>> 10) & 0x3ff) | 0xd800;
|
||||
codepoint = 0xdc00 | (codepoint & 0x3ff);
|
||||
}
|
||||
pending[pendingIndex++] = codepoint;
|
||||
} else {
|
||||
// invalid initial byte
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @param {string} string
|
||||
/// @return {Uint8Array}
|
||||
////
|
||||
/// source: https://github.com/samthor/fast-text-encoding
|
||||
export function encodeFallback(string: string): Uint8Array {
|
||||
var pos = 0;
|
||||
var len = string.length;
|
||||
|
||||
var at = 0; // output position
|
||||
var tlen = Math.max(32, len + (len >>> 1) + 7); // 1.5x size
|
||||
var target = new Uint8Array((tlen >>> 3) << 3); // ... but at 8 byte offset
|
||||
|
||||
while (pos < len) {
|
||||
var value = string.charCodeAt(pos++);
|
||||
if (value >= 0xd800 && value <= 0xdbff) {
|
||||
// high surrogate
|
||||
if (pos < len) {
|
||||
var extra = string.charCodeAt(pos);
|
||||
if ((extra & 0xfc00) === 0xdc00) {
|
||||
++pos;
|
||||
value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
|
||||
}
|
||||
}
|
||||
if (value >= 0xd800 && value <= 0xdbff) {
|
||||
continue; // drop lone surrogate
|
||||
}
|
||||
}
|
||||
|
||||
// expand the buffer if we couldn't write 4 bytes
|
||||
if (at + 4 > target.length) {
|
||||
tlen += 8; // minimum extra
|
||||
tlen *= 1.0 + (pos / string.length) * 2; // take 2x the remaining
|
||||
tlen = (tlen >>> 3) << 3; // 8 byte offset
|
||||
|
||||
var update = new Uint8Array(tlen);
|
||||
update.set(target);
|
||||
target = update;
|
||||
}
|
||||
|
||||
if ((value & 0xffffff80) === 0) {
|
||||
// 1-byte
|
||||
target[at++] = value; // ASCII
|
||||
continue;
|
||||
} else if ((value & 0xfffff800) === 0) {
|
||||
// 2-byte
|
||||
target[at++] = ((value >>> 6) & 0x1f) | 0xc0;
|
||||
} else if ((value & 0xffff0000) === 0) {
|
||||
// 3-byte
|
||||
target[at++] = ((value >>> 12) & 0x0f) | 0xe0;
|
||||
target[at++] = ((value >>> 6) & 0x3f) | 0x80;
|
||||
} else if ((value & 0xffe00000) === 0) {
|
||||
// 4-byte
|
||||
target[at++] = ((value >>> 18) & 0x07) | 0xf0;
|
||||
target[at++] = ((value >>> 12) & 0x3f) | 0x80;
|
||||
target[at++] = ((value >>> 6) & 0x3f) | 0x80;
|
||||
} else {
|
||||
continue; // out of range
|
||||
}
|
||||
|
||||
target[at++] = (value & 0x3f) | 0x80;
|
||||
}
|
||||
|
||||
// Use subarray if slice isn't supported (IE11). This will use more memory
|
||||
// because the original array still exists.
|
||||
return target.slice ? target.slice(0, at) : target.subarray(0, at);
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import { test, expect } from "vitest";
|
||||
|
||||
import { addPeriodicCallback, parsePath, query, execute, stringHandler, htmlHandler, jsonHandler, addRoute, dispatch, HttpError } from "../src/trailbase";
|
||||
import type { Method, RequestType, StringRequestType, CallbackType } from "../src/trailbase";
|
||||
import { decodeFallback, encodeFallback } from "../src/util";
|
||||
|
||||
globalThis.rustyscript = {
|
||||
async_functions: {},
|
||||
functions: {
|
||||
isolate_id: () => 0,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
install_route: (_method: Method, _route: string) => { },
|
||||
},
|
||||
};
|
||||
|
||||
test("periodic callback", async () => {
|
||||
const promise = new Promise((resolve) => {
|
||||
let count = 0;
|
||||
const result: number[] = [];
|
||||
|
||||
addPeriodicCallback(1, (cancel) => {
|
||||
result.push(count++);
|
||||
|
||||
if (result.length > 2) {
|
||||
resolve(result);
|
||||
cancel();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
expect(await promise).toEqual([0, 1, 2]);
|
||||
});
|
||||
|
||||
test("binary encode/decode", () => {
|
||||
const a = `1234567890-=qwertyuiop[]asdfghjkl;'zxcvbnm,./~!@#$%^&*()_+ `;
|
||||
expect(decodeFallback(encodeFallback(a))).toEqual(a);
|
||||
});
|
||||
|
||||
test("parse path", () => {
|
||||
const parsedPath = parsePath("/p0/p1/p2?a=x&a=y&b=z");
|
||||
expect(parsedPath.path).toEqual("/p0/p1/p2");
|
||||
const q = parsedPath.query;
|
||||
expect(q.getAll("a")).toEqual(["x", "y"]);
|
||||
expect(q.get("b")).toEqual("z");
|
||||
});
|
||||
|
||||
test("db functions", async () => {
|
||||
type Args = {
|
||||
query: string;
|
||||
params: unknown[];
|
||||
};
|
||||
let queryArgs: Args = { query: "", params: [] };
|
||||
let executeArgs: Args = { query: "", params: [] };
|
||||
|
||||
{
|
||||
const query = async (query: string, params: unknown[]) => queryArgs = { query, params };
|
||||
const execute = async (query: string, params: unknown[]) => executeArgs = { query, params };
|
||||
|
||||
globalThis.rustyscript = {
|
||||
...globalThis.rustyscript,
|
||||
async_functions: {
|
||||
query,
|
||||
execute,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const executeStr = "INSERT INTO table (col) VALUES (?1)";
|
||||
await execute(executeStr, ["test"]);
|
||||
expect(executeArgs.query).toEqual(executeStr);
|
||||
expect(executeArgs.params).toEqual(["test"]);
|
||||
|
||||
const queryStr = "SELECT * FROM table WHERE col = ?1";
|
||||
await query(queryStr, ["test"]);
|
||||
expect(queryArgs.query).toEqual(queryStr);
|
||||
expect(queryArgs.params).toEqual(["test"]);
|
||||
});
|
||||
|
||||
test("routes functions", async () => {
|
||||
const promise = new Promise<StringRequestType>((resolve) => {
|
||||
addRoute("GET", "/test", stringHandler(async (req: StringRequestType) => {
|
||||
resolve(req);
|
||||
return "response";
|
||||
}));
|
||||
});
|
||||
|
||||
const uri = "http://127.0.0.1";
|
||||
dispatch("GET", "/test", uri, [], [], undefined, encodeFallback("test"));
|
||||
|
||||
const result: StringRequestType = await promise;
|
||||
|
||||
expect(result.uri).toEqual(uri);
|
||||
});
|
||||
|
||||
test("string handler", async () => {
|
||||
const req = {
|
||||
uri: "http://test.gov",
|
||||
params: {},
|
||||
headers: {},
|
||||
} satisfies RequestType;
|
||||
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handler: CallbackType = stringHandler((_req) => "test");
|
||||
const response = (await handler(req))!;
|
||||
expect(decodeFallback(response.body!)).toEqual("test");
|
||||
}
|
||||
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handler: CallbackType = stringHandler((_req) => {
|
||||
throw new HttpError(418);
|
||||
});
|
||||
expect((await handler(req))!.status).toEqual(418);
|
||||
}
|
||||
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handler: CallbackType = htmlHandler((_req) => {
|
||||
throw new HttpError(418);
|
||||
});
|
||||
expect((await handler(req))!.status).toEqual(418);
|
||||
}
|
||||
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handler: CallbackType = jsonHandler((_req) => {
|
||||
throw new HttpError(418);
|
||||
});
|
||||
expect((await handler(req))!.status).toEqual(418);
|
||||
}
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext", "DOM"],
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": false,
|
||||
"moduleResolution": "bundler",
|
||||
"outDir": "dist",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"dist/**",
|
||||
"node_modules/",
|
||||
"types/**"
|
||||
]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
import dts from 'vite-plugin-dts'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
dts({ rollupTypes: true }),
|
||||
],
|
||||
build: {
|
||||
outDir: "./dist",
|
||||
minify: false,
|
||||
lib: {
|
||||
entry: "./src/index.ts",
|
||||
name: "runtime",
|
||||
fileName: "index",
|
||||
formats: ["es"],
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,14 +0,0 @@
|
||||
#![allow(clippy::needless_return)]
|
||||
|
||||
use std::{io::Result, path::PathBuf};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
trailbase_build::init_env_logger();
|
||||
|
||||
let path = PathBuf::from("assets").join("runtime");
|
||||
trailbase_build::rerun_if_changed(path.join("src"));
|
||||
|
||||
trailbase_build::build_js(path)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
use rustyscript::deno_core::{
|
||||
ModuleSpecifier, RequestedModuleType, ResolutionKind, error::ModuleLoaderError,
|
||||
};
|
||||
use rustyscript::module_loader::ImportProvider as RustyScriptImportProvider;
|
||||
|
||||
use crate::util::cow_to_string;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ImportProvider;
|
||||
|
||||
impl RustyScriptImportProvider for ImportProvider {
|
||||
fn resolve(
|
||||
&mut self,
|
||||
specifier: &ModuleSpecifier,
|
||||
_referrer: &str,
|
||||
_kind: ResolutionKind,
|
||||
) -> Option<Result<ModuleSpecifier, ModuleLoaderError>> {
|
||||
log::trace!("resolve: {specifier:?}");
|
||||
|
||||
// Specifier is just a URL.
|
||||
match specifier.scheme() {
|
||||
"file" | "trailbase" => {
|
||||
return Some(Ok(specifier.clone()));
|
||||
}
|
||||
scheme => {
|
||||
return Some(Err(ModuleLoaderError::uri_error(format!(
|
||||
"Unsupported schema: '{scheme}'"
|
||||
))));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn import(
|
||||
&mut self,
|
||||
specifier: &ModuleSpecifier,
|
||||
_referrer: Option<&ModuleSpecifier>,
|
||||
_is_dyn_import: bool,
|
||||
_requested_module_type: RequestedModuleType,
|
||||
) -> Option<Result<String, ModuleLoaderError>> {
|
||||
log::trace!("import: {specifier:?}");
|
||||
|
||||
match specifier.scheme() {
|
||||
"trailbase" => {
|
||||
return Some(Ok(cow_to_string(
|
||||
crate::JsRuntimeAssets::get("index.js")
|
||||
.expect("Failed to read rt/index.js")
|
||||
.data,
|
||||
)));
|
||||
}
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#![forbid(unsafe_code, clippy::unwrap_used)]
|
||||
#![allow(clippy::needless_return)]
|
||||
#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)]
|
||||
|
||||
pub mod import_provider;
|
||||
pub mod runtime;
|
||||
mod util;
|
||||
|
||||
pub use crate::import_provider::ImportProvider;
|
||||
|
||||
#[derive(rust_embed::RustEmbed, Clone)]
|
||||
#[folder = "assets/runtime/dist/"]
|
||||
pub struct JsRuntimeAssets;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub(crate) fn cow_to_string(cow: Cow<'static, [u8]>) -> String {
|
||||
match cow {
|
||||
Cow::Borrowed(x) => String::from_utf8_lossy(x).to_string(),
|
||||
Cow::Owned(x) => String::from_utf8_lossy(&x).to_string(),
|
||||
}
|
||||
}
|
||||
129
pnpm-lock.yaml
generated
129
pnpm-lock.yaml
generated
@@ -382,33 +382,6 @@ importers:
|
||||
specifier: ^8.46.4
|
||||
version: 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
|
||||
crates/js-runtime/assets/runtime:
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: ^9.39.1
|
||||
version: 9.39.1
|
||||
eslint:
|
||||
specifier: ^9.39.1
|
||||
version: 9.39.1(jiti@2.6.1)
|
||||
prettier:
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
typescript-eslint:
|
||||
specifier: ^8.46.4
|
||||
version: 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
vite:
|
||||
specifier: ^7.2.2
|
||||
version: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1)
|
||||
vite-plugin-dts:
|
||||
specifier: ^4.5.4
|
||||
version: 4.5.4(@types/node@24.10.1)(rollup@4.53.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@15.11.7)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1)
|
||||
|
||||
docs:
|
||||
dependencies:
|
||||
'@astrojs/check':
|
||||
@@ -3128,8 +3101,8 @@ packages:
|
||||
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
caniuse-lite@1.0.30001755:
|
||||
resolution: {integrity: sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==}
|
||||
caniuse-lite@1.0.30001754:
|
||||
resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==}
|
||||
|
||||
case-anything@2.1.13:
|
||||
resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==}
|
||||
@@ -3386,8 +3359,8 @@ packages:
|
||||
resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
csstype@3.2.0:
|
||||
resolution: {integrity: sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg==}
|
||||
|
||||
csv-parse@5.6.0:
|
||||
resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==}
|
||||
@@ -3524,8 +3497,8 @@ packages:
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
electron-to-chromium@1.5.254:
|
||||
resolution: {integrity: sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==}
|
||||
electron-to-chromium@1.5.252:
|
||||
resolution: {integrity: sha512-53uTpjtRgS7gjIxZ4qCgFdNO2q+wJt/Z8+xAvxbCqXPJrY6h7ighUkadQmNMXH96crtpa6gPFNP7BF4UBGDuaA==}
|
||||
|
||||
emmet@2.4.11:
|
||||
resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==}
|
||||
@@ -3840,8 +3813,8 @@ packages:
|
||||
fontkit@2.0.4:
|
||||
resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==}
|
||||
|
||||
form-data@4.0.5:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
form-data@4.0.4:
|
||||
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
@@ -4152,8 +4125,8 @@ packages:
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
inline-style-parser@0.2.7:
|
||||
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
|
||||
inline-style-parser@0.2.6:
|
||||
resolution: {integrity: sha512-gtGXVaBdl5mAes3rPcMedEBm12ibjt1kDMFfheul1wUAOVEJW60voNdMVzVkfLN06O7ZaD/rxhfKgtlgtTbMjg==}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
@@ -4262,8 +4235,8 @@ packages:
|
||||
jju@1.4.0:
|
||||
resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
|
||||
|
||||
jose@6.1.2:
|
||||
resolution: {integrity: sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==}
|
||||
jose@6.1.1:
|
||||
resolution: {integrity: sha512-GWSqjfOPf4cWOkBzw5THBjtGPhXKqYnfRBzh4Ni+ArTrQQ9unvmsA3oFLqaYKoKe5sjWmGu5wVKg9Ft1i+LQfg==}
|
||||
|
||||
js-base64@3.7.8:
|
||||
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
|
||||
@@ -5487,8 +5460,8 @@ packages:
|
||||
deprecated: 'SECURITY: Multiple vulnerabilities fixed in 8.0.1 (XML injection, path traversal, command injection, protocol injection). Upgrade immediately: npm install sitemap@8.0.1'
|
||||
hasBin: true
|
||||
|
||||
smol-toml@1.5.2:
|
||||
resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==}
|
||||
smol-toml@1.5.0:
|
||||
resolution: {integrity: sha512-Jjsa8LZ+DyLbZ7gVi9d18bS8oxq0PQrTlVDfvYXgh7gxLwbW9QWgvakHD+hBLUtr5NahfStd8LQLGSPchaEJ8Q==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
solid-devtools@0.30.1:
|
||||
@@ -5645,11 +5618,11 @@ packages:
|
||||
style-mod@4.1.3:
|
||||
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
|
||||
|
||||
style-to-js@1.1.21:
|
||||
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
|
||||
style-to-js@1.1.19:
|
||||
resolution: {integrity: sha512-Ev+SgeqiNGT1ufsXyVC5RrJRXdrkRJ1Gol9Qw7Pb72YCKJXrBvP0ckZhBeVSrw2m06DJpei2528uIpjMb4TsoQ==}
|
||||
|
||||
style-to-object@1.0.14:
|
||||
resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
|
||||
style-to-object@1.0.12:
|
||||
resolution: {integrity: sha512-ddJqYnoT4t97QvN2C95bCgt+m7AAgXjVnkk/jxAfmp7EAB8nnqqZYEbMd3em7/vEomDb2LAQKAy1RFfv41mdNw==}
|
||||
|
||||
suf-log@2.5.3:
|
||||
resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==}
|
||||
@@ -5755,11 +5728,11 @@ packages:
|
||||
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tldts-core@7.0.18:
|
||||
resolution: {integrity: sha512-jqJC13oP4FFAahv4JT/0WTDrCF9Okv7lpKtOZUGPLiAnNbACcSg8Y8T+Z9xthOmRBqi/Sob4yi0TE0miRCvF7Q==}
|
||||
tldts-core@7.0.17:
|
||||
resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==}
|
||||
|
||||
tldts@7.0.18:
|
||||
resolution: {integrity: sha512-lCcgTAgMxQ1JKOWrVGo6E69Ukbnx4Gc1wiYLRf6J5NN4HRYJtCby1rPF8rkQ4a6qqoFBK5dvjJ1zJ0F7VfDSvw==}
|
||||
tldts@7.0.17:
|
||||
resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==}
|
||||
hasBin: true
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
@@ -6629,7 +6602,7 @@ snapshots:
|
||||
remark-rehype: 11.1.2
|
||||
remark-smartypants: 3.0.2
|
||||
shiki: 3.15.0
|
||||
smol-toml: 1.5.2
|
||||
smol-toml: 1.5.0
|
||||
unified: 11.0.5
|
||||
unist-util-remove-position: 5.0.0
|
||||
unist-util-visit: 5.0.0
|
||||
@@ -8381,7 +8354,7 @@ snapshots:
|
||||
|
||||
'@types/react@19.2.5':
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
csstype: 3.2.0
|
||||
|
||||
'@types/sax@1.2.7':
|
||||
dependencies:
|
||||
@@ -8930,7 +8903,7 @@ snapshots:
|
||||
rehype: 13.0.2
|
||||
semver: 7.7.3
|
||||
shiki: 3.15.0
|
||||
smol-toml: 1.5.2
|
||||
smol-toml: 1.5.0
|
||||
tinyexec: 1.0.2
|
||||
tinyglobby: 0.2.15
|
||||
tsconfck: 3.1.6(typescript@4.9.4)
|
||||
@@ -9031,7 +9004,7 @@ snapshots:
|
||||
rehype: 13.0.2
|
||||
semver: 7.7.3
|
||||
shiki: 3.15.0
|
||||
smol-toml: 1.5.2
|
||||
smol-toml: 1.5.0
|
||||
tinyexec: 1.0.2
|
||||
tinyglobby: 0.2.15
|
||||
tsconfck: 3.1.6(typescript@5.9.3)
|
||||
@@ -9095,7 +9068,7 @@ snapshots:
|
||||
autoprefixer@10.4.22(postcss@8.5.6):
|
||||
dependencies:
|
||||
browserslist: 4.28.0
|
||||
caniuse-lite: 1.0.30001755
|
||||
caniuse-lite: 1.0.30001754
|
||||
fraction.js: 5.3.4
|
||||
normalize-range: 0.1.2
|
||||
picocolors: 1.1.1
|
||||
@@ -9105,7 +9078,7 @@ snapshots:
|
||||
axios@1.13.2:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
form-data: 4.0.5
|
||||
form-data: 4.0.4
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
@@ -9205,8 +9178,8 @@ snapshots:
|
||||
browserslist@4.28.0:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.8.28
|
||||
caniuse-lite: 1.0.30001755
|
||||
electron-to-chromium: 1.5.254
|
||||
caniuse-lite: 1.0.30001754
|
||||
electron-to-chromium: 1.5.252
|
||||
node-releases: 2.0.27
|
||||
update-browserslist-db: 1.1.4(browserslist@4.28.0)
|
||||
|
||||
@@ -9237,7 +9210,7 @@ snapshots:
|
||||
|
||||
camelcase@8.0.0: {}
|
||||
|
||||
caniuse-lite@1.0.30001755: {}
|
||||
caniuse-lite@1.0.30001754: {}
|
||||
|
||||
case-anything@2.1.13: {}
|
||||
|
||||
@@ -9507,7 +9480,7 @@ snapshots:
|
||||
'@csstools/css-syntax-patches-for-csstree': 1.0.16
|
||||
css-tree: 3.1.0
|
||||
|
||||
csstype@3.2.3: {}
|
||||
csstype@3.2.0: {}
|
||||
|
||||
csv-parse@5.6.0: {}
|
||||
|
||||
@@ -9610,7 +9583,7 @@ snapshots:
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
electron-to-chromium@1.5.254: {}
|
||||
electron-to-chromium@1.5.252: {}
|
||||
|
||||
emmet@2.4.11:
|
||||
dependencies:
|
||||
@@ -9766,7 +9739,7 @@ snapshots:
|
||||
is-html: 2.0.0
|
||||
kebab-case: 1.0.2
|
||||
known-css-properties: 0.30.0
|
||||
style-to-object: 1.0.14
|
||||
style-to-object: 1.0.12
|
||||
typescript: 4.9.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -9779,7 +9752,7 @@ snapshots:
|
||||
is-html: 2.0.0
|
||||
kebab-case: 1.0.2
|
||||
known-css-properties: 0.30.0
|
||||
style-to-object: 1.0.14
|
||||
style-to-object: 1.0.12
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -10059,7 +10032,7 @@ snapshots:
|
||||
unicode-properties: 1.4.1
|
||||
unicode-trie: 2.0.0
|
||||
|
||||
form-data@4.0.5:
|
||||
form-data@4.0.4:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
@@ -10313,7 +10286,7 @@ snapshots:
|
||||
mdast-util-mdxjs-esm: 2.0.1
|
||||
property-information: 7.1.0
|
||||
space-separated-tokens: 2.0.2
|
||||
style-to-js: 1.1.21
|
||||
style-to-js: 1.1.19
|
||||
unist-util-position: 5.0.0
|
||||
zwitch: 2.0.4
|
||||
transitivePeerDependencies:
|
||||
@@ -10347,7 +10320,7 @@ snapshots:
|
||||
mdast-util-mdxjs-esm: 2.0.1
|
||||
property-information: 7.1.0
|
||||
space-separated-tokens: 2.0.2
|
||||
style-to-js: 1.1.21
|
||||
style-to-js: 1.1.19
|
||||
unist-util-position: 5.0.0
|
||||
vfile-message: 4.0.3
|
||||
transitivePeerDependencies:
|
||||
@@ -10492,7 +10465,7 @@ snapshots:
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
inline-style-parser@0.2.7: {}
|
||||
inline-style-parser@0.2.6: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
@@ -10565,7 +10538,7 @@ snapshots:
|
||||
|
||||
jju@1.4.0: {}
|
||||
|
||||
jose@6.1.2: {}
|
||||
jose@6.1.1: {}
|
||||
|
||||
js-base64@3.7.8: {}
|
||||
|
||||
@@ -11361,7 +11334,7 @@ snapshots:
|
||||
cors: 2.8.5
|
||||
express: 5.1.0
|
||||
is-plain-obj: 4.1.0
|
||||
jose: 6.1.2
|
||||
jose: 6.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -12180,7 +12153,7 @@ snapshots:
|
||||
arg: 5.0.2
|
||||
sax: 1.4.3
|
||||
|
||||
smol-toml@1.5.2: {}
|
||||
smol-toml@1.5.0: {}
|
||||
|
||||
solid-devtools@0.30.1(solid-js@1.9.10)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1)):
|
||||
dependencies:
|
||||
@@ -12230,7 +12203,7 @@ snapshots:
|
||||
|
||||
solid-js@1.9.10:
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
csstype: 3.2.0
|
||||
seroval: 1.3.2
|
||||
seroval-plugins: 1.3.3(seroval@1.3.2)
|
||||
|
||||
@@ -12369,13 +12342,13 @@ snapshots:
|
||||
|
||||
style-mod@4.1.3: {}
|
||||
|
||||
style-to-js@1.1.21:
|
||||
style-to-js@1.1.19:
|
||||
dependencies:
|
||||
style-to-object: 1.0.14
|
||||
style-to-object: 1.0.12
|
||||
|
||||
style-to-object@1.0.14:
|
||||
style-to-object@1.0.12:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.7
|
||||
inline-style-parser: 0.2.6
|
||||
|
||||
suf-log@2.5.3:
|
||||
dependencies:
|
||||
@@ -12473,11 +12446,11 @@ snapshots:
|
||||
|
||||
tinyspy@4.0.4: {}
|
||||
|
||||
tldts-core@7.0.18: {}
|
||||
tldts-core@7.0.17: {}
|
||||
|
||||
tldts@7.0.18:
|
||||
tldts@7.0.17:
|
||||
dependencies:
|
||||
tldts-core: 7.0.18
|
||||
tldts-core: 7.0.17
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
@@ -12489,7 +12462,7 @@ snapshots:
|
||||
|
||||
tough-cookie@6.0.0:
|
||||
dependencies:
|
||||
tldts: 7.0.18
|
||||
tldts: 7.0.17
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ packages:
|
||||
- 'crates/assets/js/admin'
|
||||
- 'crates/assets/js/client'
|
||||
- 'crates/auth-ui/ui'
|
||||
- 'crates/js-runtime/assets/runtime'
|
||||
- 'docs'
|
||||
- 'docs/examples/record_api_ts'
|
||||
- 'examples/blog/web'
|
||||
|
||||
Reference in New Issue
Block a user