mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-22 12:23:04 -07:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 098f6de047 | |||
| 1b0f90fd68 | |||
| 12252f407b | |||
| 3b6e3f47ab | |||
| 6a9ac9b025 | |||
| ae6aa4088b | |||
| c08f431180 | |||
| 123c1f56e9 | |||
| 35ac65a864 | |||
| e9f362cc87 | |||
| 65685c23e1 | |||
| 2f74748cea | |||
| f477bd66f3 | |||
| d7d77ae8f0 | |||
| 31110a740d | |||
| b64d8b1d7f | |||
| c46006aacc | |||
| 92f81b1493 | |||
| 70213cfc8f | |||
| 8a82bf5c50 | |||
| 37405384a2 | |||
| 54ea6cc53b |
@@ -3,6 +3,7 @@
|
||||
|
||||
# Builds
|
||||
target
|
||||
dist
|
||||
|
||||
# Copies
|
||||
*\ copy*
|
||||
|
||||
Generated
+1249
-121
File diff suppressed because it is too large
Load Diff
+16
-15
@@ -4,7 +4,7 @@ members = ["crates/*"]
|
||||
package.description = "The Bitcoin Research Kit is a suite of tools designed to extract, compute and display data stored on a Bitcoin Core node"
|
||||
package.license = "MIT"
|
||||
package.edition = "2024"
|
||||
package.version = "0.0.52"
|
||||
package.version = "0.0.61"
|
||||
package.homepage = "https://bitcoinresearchkit.org"
|
||||
package.repository = "https://github.com/bitcoinresearchkit/brk"
|
||||
|
||||
@@ -22,26 +22,27 @@ axum = "0.8.4"
|
||||
bincode = { version = "2.0.1", features = ["serde"] }
|
||||
bitcoin = { version = "0.32.6", features = ["serde"] }
|
||||
bitcoincore-rpc = "0.19.0"
|
||||
brk_cli = { version = "0.0.52", path = "crates/brk_cli" }
|
||||
brk_computer = { version = "0.0.52", path = "crates/brk_computer" }
|
||||
brk_core = { version = "0.0.52", path = "crates/brk_core" }
|
||||
brk_exit = { version = "0.0.52", path = "crates/brk_exit" }
|
||||
brk_fetcher = { version = "0.0.52", path = "crates/brk_fetcher" }
|
||||
brk_indexer = { version = "0.0.52", path = "crates/brk_indexer" }
|
||||
brk_logger = { version = "0.0.52", path = "crates/brk_logger" }
|
||||
brk_parser = { version = "0.0.52", path = "crates/brk_parser" }
|
||||
brk_query = { version = "0.0.52", path = "crates/brk_query" }
|
||||
brk_server = { version = "0.0.52", path = "crates/brk_server" }
|
||||
brk_state = { version = "0.0.52", path = "crates/brk_state" }
|
||||
brk_store = { version = "0.0.52", path = "crates/brk_store" }
|
||||
brk_vec = { version = "0.0.52", path = "crates/brk_vec" }
|
||||
brk_bundler = { version = "0.0.61", path = "crates/brk_bundler" }
|
||||
brk_cli = { version = "0.0.61", path = "crates/brk_cli" }
|
||||
brk_computer = { version = "0.0.61", path = "crates/brk_computer" }
|
||||
brk_core = { version = "0.0.61", path = "crates/brk_core" }
|
||||
brk_exit = { version = "0.0.61", path = "crates/brk_exit" }
|
||||
brk_fetcher = { version = "0.0.61", path = "crates/brk_fetcher" }
|
||||
brk_indexer = { version = "0.0.61", path = "crates/brk_indexer" }
|
||||
brk_logger = { version = "0.0.61", path = "crates/brk_logger" }
|
||||
brk_parser = { version = "0.0.61", path = "crates/brk_parser" }
|
||||
brk_query = { version = "0.0.61", path = "crates/brk_query" }
|
||||
brk_server = { version = "0.0.61", path = "crates/brk_server" }
|
||||
brk_state = { version = "0.0.61", path = "crates/brk_state" }
|
||||
brk_store = { version = "0.0.61", path = "crates/brk_store" }
|
||||
brk_vec = { version = "0.0.61", path = "crates/brk_vec" }
|
||||
byteview = "=0.6.1"
|
||||
clap = { version = "4.5.40", features = ["string"] }
|
||||
clap_derive = "4.5.40"
|
||||
color-eyre = "0.6.5"
|
||||
derive_deref = "1.1.1"
|
||||
fjall = "2.11.0"
|
||||
jiff = "0.2.14"
|
||||
jiff = "0.2.15"
|
||||
log = { version = "0.4.27" }
|
||||
minreq = { version = "2.13.4", features = ["https", "serde_json"] }
|
||||
rayon = "1.10.0"
|
||||
|
||||
@@ -71,6 +71,7 @@ In contrast, existing alternatives tend to be either [very costly](https://studi
|
||||
- [`brk_state`](https://crates.io/crates/brk_state): Various states used mainly by the computer
|
||||
- [`brk_store`](https://crates.io/crates/brk_store): A thin wrapper around [`fjall`](https://crates.io/crates/fjall)
|
||||
- [`brk_vec`](https://crates.io/crates/brk_vec): A push-only, truncable, compressable, saveable Vec
|
||||
- [`brk_bundler`](https://crates.io/crates/brk_bundler): A thin wrapper around [`rolldown`](https://rolldown.rs/)
|
||||
|
||||
## Hosting as a service
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ version.workspace = true
|
||||
|
||||
[features]
|
||||
full = [
|
||||
"bundler",
|
||||
"core",
|
||||
"computer",
|
||||
"exit",
|
||||
@@ -23,6 +24,7 @@ full = [
|
||||
"store",
|
||||
"vec",
|
||||
]
|
||||
bundler = ["brk_bundler"]
|
||||
core = ["brk_core"]
|
||||
computer = ["brk_computer"]
|
||||
exit = ["brk_exit"]
|
||||
@@ -37,6 +39,7 @@ store = ["brk_store"]
|
||||
vec = ["brk_vec"]
|
||||
|
||||
[dependencies]
|
||||
brk_bundler = { workspace = true, optional = true }
|
||||
brk_cli = { workspace = true }
|
||||
brk_core = { workspace = true, optional = true }
|
||||
brk_computer = { workspace = true, optional = true }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
fn main() {}
|
||||
@@ -1,5 +1,12 @@
|
||||
#![doc = include_str!(concat!("../", env!("CARGO_PKG_README")))]
|
||||
|
||||
#[cfg(feature = "bundler")]
|
||||
#[doc(inline)]
|
||||
pub use brk_bundler as bundler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use brk_cli as cli;
|
||||
|
||||
#[cfg(feature = "core")]
|
||||
#[doc(inline)]
|
||||
pub use brk_core as core;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "brk_bundler"
|
||||
description = "A thin wrapper around rolldown"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
notify = "8.0.0"
|
||||
brk_rolldown = "0.0.1"
|
||||
sugar_path = "1.2.0"
|
||||
tokio = { workspace = true }
|
||||
@@ -0,0 +1,142 @@
|
||||
use std::{fs, io, path::Path, sync::Arc};
|
||||
|
||||
use brk_rolldown::{Bundler, BundlerOptions, RawMinifyOptions, SourceMapType};
|
||||
use log::error;
|
||||
use notify::{EventKind, RecursiveMode, Watcher};
|
||||
use sugar_path::SugarPath;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub async fn bundle(websites_path: &Path, source_folder: &str, watch: bool) -> io::Result<()> {
|
||||
let source_path = websites_path.join(source_folder);
|
||||
let dist_path = websites_path.join("dist");
|
||||
|
||||
let _ = fs::remove_dir_all(&dist_path);
|
||||
copy_dir_all(&source_path, &dist_path)?;
|
||||
|
||||
let source_scripts = format!("./{source_folder}/scripts");
|
||||
let source_entry = format!("{source_scripts}/entry.js");
|
||||
|
||||
let absolute_websites_path = websites_path.absolutize();
|
||||
|
||||
let mut bundler = Bundler::new(BundlerOptions {
|
||||
input: Some(vec![source_entry.into()]),
|
||||
dir: Some("./dist/scripts".to_string()),
|
||||
cwd: Some(absolute_websites_path),
|
||||
minify: Some(RawMinifyOptions::Bool(true)),
|
||||
sourcemap: Some(SourceMapType::File),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
bundler.write().await.unwrap();
|
||||
|
||||
let absolute_source_index_path = source_path.join("index.html").absolutize();
|
||||
let absolute_source_index_path_clone = absolute_source_index_path.clone();
|
||||
let absolute_source_path = source_path.absolutize();
|
||||
let absolute_source_path_clone = absolute_source_path.clone();
|
||||
let absolute_source_scripts_path = websites_path.join(source_scripts).absolutize();
|
||||
let absolute_source_sw_path = source_path.join("service-worker.js").absolutize();
|
||||
let absolute_source_sw_path_clone = absolute_source_sw_path.clone();
|
||||
|
||||
let absolute_dist_entry_path = dist_path.join("scripts/entry.js").absolutize();
|
||||
let absolute_dist_index_path = dist_path.join("index.html").absolutize();
|
||||
let absolute_dist_path = dist_path.absolutize();
|
||||
let absolute_dist_path_clone = absolute_dist_path.clone();
|
||||
let absolute_dist_sw_path = dist_path.join("service-worker.js").absolutize();
|
||||
|
||||
let write_index = move || {
|
||||
let mut contents = fs::read_to_string(&absolute_source_index_path).unwrap();
|
||||
|
||||
if let Ok(entry) = fs::read_to_string(absolute_dist_path_clone.join("scripts/entry.js")) {
|
||||
let start = entry.find("main").unwrap();
|
||||
let end = entry.find(".js").unwrap();
|
||||
let main_hashed = &entry[start..end];
|
||||
contents = contents.replace("/scripts/main.js", &format!("/scripts/{main_hashed}.js"));
|
||||
}
|
||||
|
||||
let _ = fs::write(&absolute_dist_index_path, contents);
|
||||
};
|
||||
|
||||
let write_sw = move || {
|
||||
let contents = fs::read_to_string(&absolute_source_sw_path)
|
||||
.unwrap()
|
||||
.replace("__VERSION__", &format!("v{VERSION}"));
|
||||
let _ = fs::write(&absolute_dist_sw_path, contents);
|
||||
};
|
||||
|
||||
write_index();
|
||||
write_sw();
|
||||
|
||||
if !watch {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
let write_index_clone = write_index.clone();
|
||||
|
||||
let mut entry_watcher = notify::recommended_watcher(
|
||||
move |res: Result<notify::Event, notify::Error>| match res {
|
||||
Ok(_) => write_index_clone(),
|
||||
Err(e) => error!("watch error: {:?}", e),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
entry_watcher
|
||||
.watch(&absolute_dist_entry_path, RecursiveMode::Recursive)
|
||||
.unwrap();
|
||||
|
||||
let mut source_watcher = notify::recommended_watcher(
|
||||
move |res: Result<notify::Event, notify::Error>| match res {
|
||||
Ok(event) => match event.kind {
|
||||
EventKind::Create(_) => event.paths,
|
||||
EventKind::Modify(_) => event.paths,
|
||||
_ => vec![],
|
||||
}
|
||||
.into_iter()
|
||||
.filter(|path| path.starts_with(&absolute_source_path))
|
||||
.filter(|path| !path.starts_with(&absolute_source_scripts_path))
|
||||
.for_each(|source_path| {
|
||||
let suffix = source_path.strip_prefix(&absolute_source_path).unwrap();
|
||||
let dist_path = absolute_dist_path.join(suffix);
|
||||
|
||||
if source_path == absolute_source_index_path_clone {
|
||||
write_index();
|
||||
} else if source_path == absolute_source_sw_path_clone {
|
||||
write_sw();
|
||||
} else {
|
||||
let _ = fs::copy(&source_path, &dist_path);
|
||||
}
|
||||
}),
|
||||
Err(e) => error!("watch error: {:?}", e),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
source_watcher
|
||||
.watch(&absolute_source_path_clone, RecursiveMode::Recursive)
|
||||
.unwrap();
|
||||
|
||||
let watcher =
|
||||
brk_rolldown::Watcher::new(vec![Arc::new(Mutex::new(bundler))], None).unwrap();
|
||||
|
||||
watcher.start().await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
|
||||
fs::create_dir_all(&dst)?;
|
||||
for entry in fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let ty = entry.file_type()?;
|
||||
if ty.is_dir() {
|
||||
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
||||
} else {
|
||||
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -10,7 +10,7 @@ pub fn query(params: QueryParams) -> color_eyre::Result<()> {
|
||||
|
||||
let format = config.format();
|
||||
|
||||
let mut indexer = Indexer::new(&config.outputsdir(), format, config.check_collisions())?;
|
||||
let mut indexer = Indexer::new(&config.outputsdir(), config.check_collisions())?;
|
||||
indexer.import_vecs()?;
|
||||
|
||||
let mut computer = Computer::new(&config.outputsdir(), config.fetcher(), format);
|
||||
|
||||
+44
-28
@@ -7,7 +7,7 @@ use std::{
|
||||
|
||||
use bitcoincore_rpc::{self, Auth, Client, RpcApi};
|
||||
use brk_computer::Computer;
|
||||
use brk_core::{default_bitcoin_path, default_brk_path, dot_brk_path};
|
||||
use brk_core::{default_bitcoin_path, default_brk_path, default_on_error, dot_brk_path};
|
||||
use brk_exit::Exit;
|
||||
use brk_fetcher::Fetcher;
|
||||
use brk_indexer::Indexer;
|
||||
@@ -29,7 +29,7 @@ pub fn run(config: RunConfig) -> color_eyre::Result<()> {
|
||||
|
||||
let format = config.format();
|
||||
|
||||
let mut indexer = Indexer::new(&config.outputsdir(), format, config.check_collisions())?;
|
||||
let mut indexer = Indexer::new(&config.outputsdir(), config.check_collisions())?;
|
||||
indexer.import_stores()?;
|
||||
indexer.import_vecs()?;
|
||||
|
||||
@@ -63,8 +63,9 @@ pub fn run(config: RunConfig) -> color_eyre::Result<()> {
|
||||
|
||||
let server = Server::new(served_indexer, served_computer, config.website())?;
|
||||
|
||||
let watch = config.watch();
|
||||
let opt = Some(tokio::spawn(async move {
|
||||
server.serve().await.unwrap();
|
||||
server.serve(watch).await.unwrap();
|
||||
}));
|
||||
|
||||
sleep(Duration::from_secs(1));
|
||||
@@ -109,62 +110,82 @@ pub fn run(config: RunConfig) -> color_eyre::Result<()> {
|
||||
#[derive(Parser, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
|
||||
pub struct RunConfig {
|
||||
/// Bitcoin main directory path, defaults: ~/.bitcoin, ~/Library/Application\ Support/Bitcoin, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(long, value_name = "PATH")]
|
||||
bitcoindir: Option<String>,
|
||||
|
||||
/// Bitcoin blocks directory path, default: --bitcoindir/blocks, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(long, value_name = "PATH")]
|
||||
blocksdir: Option<String>,
|
||||
|
||||
/// Bitcoin Research Kit outputs directory path, default: ~/.brk, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(long, value_name = "PATH")]
|
||||
brkdir: Option<String>,
|
||||
|
||||
/// Executed by the runner, default: all, saved
|
||||
/// Activated services, default: all, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(short, long)]
|
||||
mode: Option<Mode>,
|
||||
services: Option<Services>,
|
||||
|
||||
/// Computation mode for compatible datasets, `lazy` computes data whenever requested without saving it, `eager` computes the data once and saves it to disk, default: Lazy, saved
|
||||
/// Computation of computed datasets, `lazy` computes data whenever requested without saving it, `eager` computes the data once and saves it to disk, default: `lazy`, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(short, long)]
|
||||
computation: Option<Computation>,
|
||||
|
||||
/// Activate compression of datasets, set to true to save disk space or false if prioritize speed, default: compressed, saved
|
||||
#[arg(short, long, value_name = "FORMAT")]
|
||||
/// Format of computed datasets, `compressed` to save disk space (experimental), `raw` to prioritize speed, default: `raw`, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(short, long)]
|
||||
format: Option<Format>,
|
||||
|
||||
/// Activate fetching prices from exchanges APIs and the computation of all related datasets, default: true, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(short = 'F', long, value_name = "BOOL")]
|
||||
fetch: Option<bool>,
|
||||
|
||||
/// Website served by the server (if active), default: default, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(short, long)]
|
||||
website: Option<Website>,
|
||||
|
||||
/// Bitcoin RPC ip, default: localhost, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(long, value_name = "IP")]
|
||||
rpcconnect: Option<String>,
|
||||
|
||||
/// Bitcoin RPC port, default: 8332, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(long, value_name = "PORT")]
|
||||
rpcport: Option<u16>,
|
||||
|
||||
/// Bitcoin RPC cookie file, default: --bitcoindir/.cookie, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(long, value_name = "PATH")]
|
||||
rpccookiefile: Option<String>,
|
||||
|
||||
/// Bitcoin RPC username, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(long, value_name = "USERNAME")]
|
||||
rpcuser: Option<String>,
|
||||
|
||||
/// Bitcoin RPC password, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(long, value_name = "PASSWORD")]
|
||||
rpcpassword: Option<String>,
|
||||
|
||||
/// Delay between runs, default: 0, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(long, value_name = "SECONDS")]
|
||||
delay: Option<u64>,
|
||||
|
||||
/// DEV: Activate to watch the selected website's folder for changes, default: false, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(long, value_name = "BOOL")]
|
||||
watch: Option<bool>,
|
||||
|
||||
/// DEV: Activate checking address hashes for collisions when indexing, default: false, saved
|
||||
#[serde(default, deserialize_with = "default_on_error")]
|
||||
#[arg(long, value_name = "BOOL")]
|
||||
check_collisions: Option<bool>,
|
||||
}
|
||||
@@ -192,8 +213,8 @@ impl RunConfig {
|
||||
config_saved.brkdir = Some(brkdir);
|
||||
}
|
||||
|
||||
if let Some(mode) = config_args.mode.take() {
|
||||
config_saved.mode = Some(mode);
|
||||
if let Some(services) = config_args.services.take() {
|
||||
config_saved.services = Some(services);
|
||||
}
|
||||
|
||||
if let Some(computation) = config_args.computation.take() {
|
||||
@@ -240,6 +261,10 @@ impl RunConfig {
|
||||
config_saved.check_collisions = Some(check_collisions);
|
||||
}
|
||||
|
||||
if let Some(watch) = config_args.watch.take() {
|
||||
config_saved.watch = Some(watch);
|
||||
}
|
||||
|
||||
if config_args != RunConfig::default() {
|
||||
dbg!(config_args);
|
||||
panic!("Didn't consume the full config")
|
||||
@@ -252,19 +277,6 @@ impl RunConfig {
|
||||
|
||||
config.write(&path)?;
|
||||
|
||||
// info!("Configuration {{");
|
||||
// info!(" bitcoindir: {:?}", config.bitcoindir);
|
||||
// info!(" brkdir: {:?}", config.brkdir);
|
||||
// info!(" mode: {:?}", config.mode);
|
||||
// info!(" website: {:?}", config.website);
|
||||
// info!(" rpcconnect: {:?}", config.rpcconnect);
|
||||
// info!(" rpcport: {:?}", config.rpcport);
|
||||
// info!(" rpccookiefile: {:?}", config.rpccookiefile);
|
||||
// info!(" rpcuser: {:?}", config.rpcuser);
|
||||
// info!(" rpcpassword: {:?}", config.rpcpassword);
|
||||
// info!(" delay: {:?}", config.delay);
|
||||
// info!("}}");
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -375,13 +387,13 @@ impl RunConfig {
|
||||
}
|
||||
|
||||
pub fn process(&self) -> bool {
|
||||
self.mode
|
||||
.is_none_or(|m| m == Mode::All || m == Mode::Processor)
|
||||
self.services
|
||||
.is_none_or(|m| m == Services::All || m == Services::Processor)
|
||||
}
|
||||
|
||||
pub fn serve(&self) -> bool {
|
||||
self.mode
|
||||
.is_none_or(|m| m == Mode::All || m == Mode::Server)
|
||||
self.services
|
||||
.is_none_or(|m| m == Services::All || m == Services::Server)
|
||||
}
|
||||
|
||||
fn path_cookiefile(&self) -> PathBuf {
|
||||
@@ -433,6 +445,10 @@ impl RunConfig {
|
||||
pub fn check_collisions(&self) -> bool {
|
||||
self.check_collisions.is_some_and(|b| b)
|
||||
}
|
||||
|
||||
pub fn watch(&self) -> bool {
|
||||
self.watch.is_some_and(|b| b)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@@ -449,7 +465,7 @@ impl RunConfig {
|
||||
PartialOrd,
|
||||
Ord,
|
||||
)]
|
||||
pub enum Mode {
|
||||
pub enum Services {
|
||||
#[default]
|
||||
All,
|
||||
Processor,
|
||||
|
||||
@@ -33,7 +33,7 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let format = Format::Raw;
|
||||
|
||||
let mut indexer = Indexer::new(outputs_dir, format, true)?;
|
||||
let mut indexer = Indexer::new(outputs_dir, true)?;
|
||||
indexer.import_stores()?;
|
||||
indexer.import_vecs()?;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use brk_core::{
|
||||
use brk_exit::Exit;
|
||||
use brk_fetcher::Fetcher;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_vec::{AnyCollectableVec, AnyIterableVec, Computation, EagerVec, Format};
|
||||
use brk_vec::{AnyCollectableVec, AnyIterableVec, Computation, EagerVec, Format, StoredIndex};
|
||||
|
||||
use super::{
|
||||
Indexes,
|
||||
@@ -429,8 +429,18 @@ impl Vecs {
|
||||
self.dateindex_to_ohlc_in_cents.compute_transform(
|
||||
starting_indexes.dateindex,
|
||||
&indexes.dateindex_to_date,
|
||||
|(di, d, ..)| {
|
||||
let ohlc = fetcher.get_date(d).unwrap();
|
||||
|(di, d, this)| {
|
||||
let mut ohlc = fetcher.get_date(d).unwrap();
|
||||
if let Some(prev) = di.decremented() {
|
||||
let prev_open = *this
|
||||
.get_or_read(prev, &this.mmap().load())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.close;
|
||||
*ohlc.open = prev_open;
|
||||
*ohlc.high = (*ohlc.high).max(prev_open);
|
||||
*ohlc.low = (*ohlc.low).min(prev_open);
|
||||
}
|
||||
(di, ohlc)
|
||||
},
|
||||
exit,
|
||||
|
||||
@@ -226,7 +226,11 @@ impl ComputedValueVecsFromTxindex {
|
||||
pub fn vecs(&self) -> Vec<&dyn AnyCollectableVec> {
|
||||
[
|
||||
self.sats.vecs(),
|
||||
vec![&self.bitcoin_txindex as &dyn AnyCollectableVec],
|
||||
self.bitcoin.vecs(),
|
||||
self.dollars_txindex
|
||||
.as_ref()
|
||||
.map_or(vec![], |v| vec![v as &dyn AnyCollectableVec]),
|
||||
self.dollars.as_ref().map_or(vec![], |v| v.vecs()),
|
||||
]
|
||||
.into_iter()
|
||||
|
||||
@@ -124,6 +124,7 @@ impl Vecs {
|
||||
fetcher: Option<&mut Fetcher>,
|
||||
exit: &Exit,
|
||||
) -> color_eyre::Result<()> {
|
||||
info!("Computing indexes...");
|
||||
let starting_indexes = self.indexes.compute(indexer, starting_indexes, exit)?;
|
||||
|
||||
info!("Computing constants...");
|
||||
|
||||
@@ -1289,7 +1289,7 @@ impl Vecs {
|
||||
base_version + self.height_to_opreturn_supply.inner_version(),
|
||||
)?;
|
||||
|
||||
let mut chain_state: Vec<BlockState>;
|
||||
let mut chain_state: Vec<BlockState> = vec![];
|
||||
let mut chain_state_starting_height = Height::from(self.chain_state.len());
|
||||
|
||||
let stateful_starting_height = match separate_utxo_vecs
|
||||
@@ -1322,25 +1322,27 @@ impl Vecs {
|
||||
.collect::<Vec<_>>();
|
||||
chain_state_starting_height
|
||||
}
|
||||
Ordering::Less => {
|
||||
// todo!("rollback instead");
|
||||
chain_state = vec![];
|
||||
chain_state_starting_height = Height::ZERO;
|
||||
Height::ZERO
|
||||
}
|
||||
Ordering::Less => Height::ZERO,
|
||||
};
|
||||
if stateful_starting_height.is_zero() {
|
||||
info!("Starting processing utxos from the start");
|
||||
separate_utxo_vecs
|
||||
.par_iter_mut()
|
||||
.try_for_each(|(_, v)| v.state.price_to_amount.reset())?;
|
||||
}
|
||||
|
||||
let starting_height = starting_indexes
|
||||
.height
|
||||
.min(stateful_starting_height)
|
||||
.min(Height::from(self.height_to_unspendable_supply.len()))
|
||||
.min(Height::from(self.height_to_opreturn_supply.len()));
|
||||
|
||||
if starting_height.is_zero() {
|
||||
info!("Starting processing utxos from the start");
|
||||
|
||||
// todo!("rollback instead");
|
||||
chain_state = vec![];
|
||||
chain_state_starting_height = Height::ZERO;
|
||||
|
||||
separate_utxo_vecs
|
||||
.par_iter_mut()
|
||||
.try_for_each(|(_, v)| v.state.price_to_amount.reset())?;
|
||||
}
|
||||
|
||||
if starting_height == Height::from(height_to_date_fixed.len()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::ops::{Add, Div};
|
||||
|
||||
use derive_deref::Deref;
|
||||
use derive_deref::{Deref, DerefMut};
|
||||
use serde::{Serialize, Serializer, ser::SerializeTuple};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
@@ -172,6 +172,7 @@ impl From<Close<Sats>> for OHLCSats {
|
||||
IntoBytes,
|
||||
KnownLayout,
|
||||
Deref,
|
||||
DerefMut,
|
||||
Serialize,
|
||||
)]
|
||||
#[repr(C)]
|
||||
@@ -259,6 +260,7 @@ where
|
||||
IntoBytes,
|
||||
KnownLayout,
|
||||
Deref,
|
||||
DerefMut,
|
||||
Serialize,
|
||||
)]
|
||||
#[repr(C)]
|
||||
@@ -346,6 +348,7 @@ where
|
||||
IntoBytes,
|
||||
KnownLayout,
|
||||
Deref,
|
||||
DerefMut,
|
||||
Serialize,
|
||||
)]
|
||||
#[repr(C)]
|
||||
@@ -433,6 +436,7 @@ where
|
||||
IntoBytes,
|
||||
KnownLayout,
|
||||
Deref,
|
||||
DerefMut,
|
||||
Serialize,
|
||||
)]
|
||||
#[repr(C)]
|
||||
|
||||
@@ -3,9 +3,11 @@ mod checked_sub;
|
||||
mod paths;
|
||||
mod pause;
|
||||
mod rlimit;
|
||||
mod serde;
|
||||
|
||||
pub use bytes::*;
|
||||
pub use checked_sub::*;
|
||||
pub use paths::*;
|
||||
pub use pause::*;
|
||||
pub use rlimit::*;
|
||||
pub use serde::*;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
pub fn default_on_error<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: Deserialize<'de> + Default,
|
||||
{
|
||||
match T::deserialize(deserializer) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(_) => Ok(T::default()),
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use brk_core::{Date, Height};
|
||||
use brk_fetcher::{BRK, Fetcher};
|
||||
use brk_fetcher::{BRK, Binance, Fetcher, Kraken};
|
||||
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
@@ -12,6 +12,19 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let mut fetcher = Fetcher::import(None)?;
|
||||
|
||||
Binance::fetch_1d().map(|b| {
|
||||
dbg!(b.last_key_value());
|
||||
})?;
|
||||
Kraken::fetch_1d().map(|b| {
|
||||
dbg!(b.last_key_value());
|
||||
})?;
|
||||
Binance::fetch_1mn().map(|b| {
|
||||
dbg!(b.last_key_value());
|
||||
})?;
|
||||
Kraken::fetch_1mn().map(|b| {
|
||||
dbg!(b.last_key_value());
|
||||
})?;
|
||||
|
||||
dbg!(fetcher.get_date(Date::new(2025, 6, 5))?);
|
||||
dbg!(fetcher.get_height(
|
||||
899911_u32.into(),
|
||||
|
||||
@@ -32,7 +32,7 @@ impl Kraken {
|
||||
)
|
||||
}
|
||||
|
||||
fn fetch_1mn() -> color_eyre::Result<BTreeMap<Timestamp, OHLCCents>> {
|
||||
pub fn fetch_1mn() -> color_eyre::Result<BTreeMap<Timestamp, OHLCCents>> {
|
||||
info!("Fetching 1mn prices from Kraken...");
|
||||
|
||||
retry(
|
||||
@@ -54,7 +54,7 @@ impl Kraken {
|
||||
.ok_or(color_eyre::eyre::Error::msg("Couldn't find date"))
|
||||
}
|
||||
|
||||
fn fetch_1d() -> color_eyre::Result<BTreeMap<Date, OHLCCents>> {
|
||||
pub fn fetch_1d() -> color_eyre::Result<BTreeMap<Date, OHLCCents>> {
|
||||
info!("Fetching daily prices from Kraken...");
|
||||
|
||||
retry(
|
||||
|
||||
@@ -40,11 +40,11 @@ impl Fetcher {
|
||||
}
|
||||
|
||||
fn get_date_(&mut self, date: Date, tries: usize) -> color_eyre::Result<OHLCCents> {
|
||||
self.binance
|
||||
self.kraken
|
||||
.get_from_1d(&date)
|
||||
.or_else(|_| {
|
||||
// eprintln!("{e}");
|
||||
self.kraken.get_from_1d(&date)
|
||||
self.binance.get_from_1d(&date)
|
||||
})
|
||||
.or_else(|_| {
|
||||
// eprintln!("{e}");
|
||||
@@ -90,11 +90,11 @@ impl Fetcher {
|
||||
let previous_timestamp = previous_timestamp.map(|t| t.floor_seconds());
|
||||
|
||||
let ohlc = self
|
||||
.binance
|
||||
.kraken
|
||||
.get_from_1mn(timestamp, previous_timestamp)
|
||||
.unwrap_or_else(|_report| {
|
||||
// eprintln!("{_report}");
|
||||
self.kraken
|
||||
self.binance
|
||||
.get_from_1mn(timestamp, previous_timestamp)
|
||||
.unwrap_or_else(|_report| {
|
||||
// // eprintln!("{_report}");
|
||||
@@ -185,8 +185,8 @@ How to fix this:
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.kraken.clear();
|
||||
self.binance.clear();
|
||||
self.brk.clear();
|
||||
self.kraken.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use brk_core::default_bitcoin_path;
|
||||
use brk_exit::Exit;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_parser::Parser;
|
||||
use brk_vec::Format;
|
||||
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
@@ -25,7 +24,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let outputs = Path::new("../../_outputs");
|
||||
|
||||
let mut indexer = Indexer::new(outputs, Format::Raw, false)?;
|
||||
let mut indexer = Indexer::new(outputs, false)?;
|
||||
|
||||
indexer.import_stores()?;
|
||||
indexer.import_vecs()?;
|
||||
|
||||
@@ -110,13 +110,7 @@ impl TryFrom<(&mut Vecs, &Stores, &Client)> for Indexes {
|
||||
vecs.height_to_blockhash
|
||||
.iter()
|
||||
.get(*height)
|
||||
.is_none_or(|saved_blockhash| {
|
||||
let b = &rpc_blockhash != saved_blockhash.as_ref();
|
||||
if b {
|
||||
dbg!(rpc_blockhash, saved_blockhash.as_ref());
|
||||
}
|
||||
b
|
||||
})
|
||||
.is_none_or(|saved_blockhash| &rpc_blockhash != saved_blockhash.as_ref())
|
||||
})
|
||||
.unwrap_or(starting_height);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ use brk_core::{
|
||||
use bitcoin::{Transaction, TxIn, TxOut};
|
||||
use brk_exit::Exit;
|
||||
use brk_parser::Parser;
|
||||
use brk_vec::{AnyVec, Format, VecIterator};
|
||||
use brk_vec::{AnyVec, VecIterator};
|
||||
use color_eyre::eyre::{ContextCompat, eyre};
|
||||
use fjall::TransactionalKeyspace;
|
||||
use log::{error, info};
|
||||
@@ -42,21 +42,15 @@ pub struct Indexer {
|
||||
vecs: Option<Vecs>,
|
||||
stores: Option<Stores>,
|
||||
check_collisions: bool,
|
||||
format: Format,
|
||||
}
|
||||
|
||||
impl Indexer {
|
||||
pub fn new(
|
||||
outputs_dir: &Path,
|
||||
format: Format,
|
||||
check_collisions: bool,
|
||||
) -> color_eyre::Result<Self> {
|
||||
pub fn new(outputs_dir: &Path, check_collisions: bool) -> color_eyre::Result<Self> {
|
||||
setrlimit()?;
|
||||
Ok(Self {
|
||||
path: outputs_dir.to_owned(),
|
||||
vecs: None,
|
||||
stores: None,
|
||||
format,
|
||||
check_collisions,
|
||||
})
|
||||
}
|
||||
@@ -65,7 +59,6 @@ impl Indexer {
|
||||
self.vecs = Some(Vecs::forced_import(
|
||||
&self.path.join("vecs/indexed"),
|
||||
VERSION + Version::ZERO,
|
||||
self.format,
|
||||
)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -66,11 +66,7 @@ pub struct Vecs {
|
||||
}
|
||||
|
||||
impl Vecs {
|
||||
pub fn forced_import(
|
||||
path: &Path,
|
||||
version: Version,
|
||||
format: Format,
|
||||
) -> color_eyre::Result<Self> {
|
||||
pub fn forced_import(path: &Path, version: Version) -> color_eyre::Result<Self> {
|
||||
fs::create_dir_all(path)?;
|
||||
|
||||
Ok(Self {
|
||||
@@ -78,7 +74,7 @@ impl Vecs {
|
||||
path,
|
||||
"txindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_blockhash: IndexedVec::forced_import(
|
||||
path,
|
||||
@@ -90,145 +86,145 @@ impl Vecs {
|
||||
path,
|
||||
"difficulty",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_emptyoutputindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_emptyoutputindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_inputindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_inputindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_opreturnindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_opreturnindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_outputindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_outputindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_p2aindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_p2aindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_p2msindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_p2msindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_p2pk33index: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_p2pk33index",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_p2pk65index: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_p2pk65index",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_p2pkhindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_p2pkhindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_p2shindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_p2shindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_p2trindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_p2trindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_p2wpkhindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_p2wpkhindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_p2wshindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_p2wshindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_txindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_txindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_first_unknownoutputindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_unknownoutputindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_timestamp: IndexedVec::forced_import(
|
||||
path,
|
||||
"timestamp",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_total_size: IndexedVec::forced_import(
|
||||
path,
|
||||
"total_size",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
height_to_weight: IndexedVec::forced_import(
|
||||
path,
|
||||
"weight",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
inputindex_to_outputindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"outputindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
opreturnindex_to_txindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"txindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
outputindex_to_outputtype: IndexedVec::forced_import(
|
||||
path,
|
||||
"outputtype",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
outputindex_to_outputtypeindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"outputtypeindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
outputindex_to_value: IndexedVec::forced_import(
|
||||
path,
|
||||
"value",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
p2aindex_to_p2abytes: IndexedVec::forced_import(
|
||||
path,
|
||||
@@ -240,7 +236,7 @@ impl Vecs {
|
||||
path,
|
||||
"txindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
p2pk33index_to_p2pk33bytes: IndexedVec::forced_import(
|
||||
path,
|
||||
@@ -288,13 +284,13 @@ impl Vecs {
|
||||
path,
|
||||
"base_size",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
txindex_to_first_inputindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"first_inputindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
txindex_to_first_outputindex: IndexedVec::forced_import(
|
||||
path,
|
||||
@@ -306,19 +302,19 @@ impl Vecs {
|
||||
path,
|
||||
"is_explicitly_rbf",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
txindex_to_rawlocktime: IndexedVec::forced_import(
|
||||
path,
|
||||
"rawlocktime",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
txindex_to_total_size: IndexedVec::forced_import(
|
||||
path,
|
||||
"total_size",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
txindex_to_txid: IndexedVec::forced_import(
|
||||
path,
|
||||
@@ -330,13 +326,13 @@ impl Vecs {
|
||||
path,
|
||||
"txversion",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
unknownoutputindex_to_txindex: IndexedVec::forced_import(
|
||||
path,
|
||||
"txindex",
|
||||
version + VERSION + Version::ZERO,
|
||||
format,
|
||||
Format::Raw,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,50 +25,53 @@ pub fn init(path: Option<&Path>) {
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
Builder::from_env(Env::default().default_filter_or("info,fjall=off,lsm_tree=off"))
|
||||
.format(move |buf, record| {
|
||||
let date_time = Timestamp::now()
|
||||
.to_zoned(tz::TimeZone::system())
|
||||
.strftime("%Y-%m-%d %H:%M:%S")
|
||||
.to_string();
|
||||
let level = record.level().as_str().to_lowercase();
|
||||
let level = format!("{:5}", level);
|
||||
let target = record.target();
|
||||
let dash = "-";
|
||||
let args = record.args();
|
||||
Builder::from_env(
|
||||
Env::default()
|
||||
.default_filter_or("info,fjall=off,lsm_tree=off,rolldown=off,brk_rolldown=off"),
|
||||
)
|
||||
.format(move |buf, record| {
|
||||
let date_time = Timestamp::now()
|
||||
.to_zoned(tz::TimeZone::system())
|
||||
.strftime("%Y-%m-%d %H:%M:%S")
|
||||
.to_string();
|
||||
let level = record.level().as_str().to_lowercase();
|
||||
let level = format!("{:5}", level);
|
||||
let target = record.target();
|
||||
let dash = "-";
|
||||
let args = record.args();
|
||||
|
||||
if let Some(file) = file.as_ref() {
|
||||
let _ = write(
|
||||
file.try_clone().unwrap(),
|
||||
&date_time,
|
||||
target,
|
||||
&level,
|
||||
dash,
|
||||
args,
|
||||
);
|
||||
}
|
||||
|
||||
let colored_date_time = date_time.bright_black();
|
||||
let colored_level = match level.chars().next().unwrap() {
|
||||
'e' => level.red().to_string(),
|
||||
'w' => level.yellow().to_string(),
|
||||
'i' => level.green().to_string(),
|
||||
'd' => level.blue().to_string(),
|
||||
't' => level.cyan().to_string(),
|
||||
_ => panic!(),
|
||||
};
|
||||
let colored_dash = dash.bright_black();
|
||||
|
||||
write(
|
||||
buf,
|
||||
colored_date_time,
|
||||
if let Some(file) = file.as_ref() {
|
||||
let _ = write(
|
||||
file.try_clone().unwrap(),
|
||||
&date_time,
|
||||
target,
|
||||
colored_level,
|
||||
colored_dash,
|
||||
&level,
|
||||
dash,
|
||||
args,
|
||||
)
|
||||
})
|
||||
.init();
|
||||
);
|
||||
}
|
||||
|
||||
let colored_date_time = date_time.bright_black();
|
||||
let colored_level = match level.chars().next().unwrap() {
|
||||
'e' => level.red().to_string(),
|
||||
'w' => level.yellow().to_string(),
|
||||
'i' => level.green().to_string(),
|
||||
'd' => level.blue().to_string(),
|
||||
't' => level.cyan().to_string(),
|
||||
_ => panic!(),
|
||||
};
|
||||
let colored_dash = dash.bright_black();
|
||||
|
||||
write(
|
||||
buf,
|
||||
colored_date_time,
|
||||
target,
|
||||
colored_level,
|
||||
colored_dash,
|
||||
args,
|
||||
)
|
||||
})
|
||||
.init();
|
||||
}
|
||||
|
||||
fn write(
|
||||
@@ -80,5 +83,9 @@ fn write(
|
||||
args: impl Display,
|
||||
) -> Result<(), std::io::Error> {
|
||||
writeln!(buf, "{} {} {} {}", date_time, dash, level, args)
|
||||
// writeln!(buf, "{} {} {} {} {}", date_time, _target, level, dash, args)
|
||||
// writeln!(
|
||||
// buf,
|
||||
// "{} {} {} {} {}",
|
||||
// date_time, _target, level, dash, args
|
||||
// )
|
||||
}
|
||||
|
||||
@@ -96,9 +96,10 @@ impl BlkIndexToBlkRecap {
|
||||
}
|
||||
|
||||
pub fn export(&self) {
|
||||
let file = File::create(&self.path).unwrap_or_else(|_| {
|
||||
let file = File::create(&self.path).unwrap_or_else(|e| {
|
||||
dbg!(e);
|
||||
dbg!(&self.path);
|
||||
panic!("No such file or directory")
|
||||
panic!("Cannot write file");
|
||||
});
|
||||
|
||||
serde_json::to_writer(&mut BufWriter::new(file), &self.tree).unwrap();
|
||||
|
||||
@@ -18,5 +18,5 @@ color-eyre = { workspace = true }
|
||||
derive_deref = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = "3.12.0"
|
||||
serde_with = "3.13.0"
|
||||
tabled = { workspace = true }
|
||||
|
||||
@@ -12,7 +12,7 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let format = Format::Compressed;
|
||||
|
||||
let mut indexer = Indexer::new(outputs_dir, format, true)?;
|
||||
let mut indexer = Indexer::new(outputs_dir, true)?;
|
||||
indexer.import_vecs()?;
|
||||
|
||||
let mut computer = Computer::new(outputs_dir, None, format);
|
||||
|
||||
@@ -10,6 +10,7 @@ repository.workspace = true
|
||||
[dependencies]
|
||||
axum = { workspace = true }
|
||||
bitcoincore-rpc = { workspace = true }
|
||||
brk_bundler = { workspace = true }
|
||||
brk_computer = { workspace = true }
|
||||
brk_exit = { workspace = true }
|
||||
brk_core = { workspace = true }
|
||||
@@ -25,11 +26,11 @@ color-eyre = { workspace = true }
|
||||
jiff = { workspace = true }
|
||||
log = { workspace = true }
|
||||
minreq = { workspace = true }
|
||||
oxc = { version = "0.72.3", features = ["codegen", "minifier"] }
|
||||
oxc = { version = "0.73.0", features = ["codegen", "minifier"] }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tower-http = { version = "0.6.6", features = ["compression-full", "trace"] }
|
||||
zip = "4.0.0"
|
||||
zip = "4.1.0"
|
||||
tracing = "0.1.41"
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let format = Format::Compressed;
|
||||
|
||||
let mut indexer = Indexer::new(outputs_dir, format, true)?;
|
||||
let mut indexer = Indexer::new(outputs_dir, true)?;
|
||||
indexer.import_stores()?;
|
||||
indexer.import_vecs()?;
|
||||
|
||||
@@ -51,7 +51,7 @@ pub fn main() -> color_eyre::Result<()> {
|
||||
let server = Server::new(served_indexer, served_computer, Website::Default)?;
|
||||
|
||||
let server = tokio::spawn(async move {
|
||||
server.serve().await.unwrap();
|
||||
server.serve(true).await.unwrap();
|
||||
});
|
||||
|
||||
if process {
|
||||
|
||||
@@ -124,6 +124,7 @@ pub async fn variant_handler(
|
||||
Query(params_opt): Query<ParamsOpt>,
|
||||
state: State<AppState>,
|
||||
) -> Response {
|
||||
let variant = variant.replace("_", "-");
|
||||
let mut split = variant.split(TO_SEPARATOR);
|
||||
let params = Params::from((
|
||||
(
|
||||
|
||||
@@ -13,8 +13,6 @@ use crate::{
|
||||
traits::{HeaderMapExtended, ModifiedState, ResponseExtended},
|
||||
};
|
||||
|
||||
use super::minify::minify_js;
|
||||
|
||||
pub async fn file_handler(
|
||||
headers: HeaderMap,
|
||||
State(app_state): State<AppState>,
|
||||
@@ -32,16 +30,12 @@ fn any_handler(
|
||||
app_state: AppState,
|
||||
path: Option<extract::Path<String>>,
|
||||
) -> Response {
|
||||
let website_path = app_state
|
||||
.websites_path
|
||||
.as_ref()
|
||||
.expect("Should never reach here is websites_path is None")
|
||||
.join(app_state.website.to_folder_name());
|
||||
let dist_path = app_state.dist_path();
|
||||
|
||||
if let Some(path) = path.as_ref() {
|
||||
let path = path.0.replace("..", "").replace("\\", "");
|
||||
|
||||
let mut path = website_path.join(&path);
|
||||
let mut path = dist_path.join(&path);
|
||||
|
||||
if !path.exists() || path.is_dir() {
|
||||
if path.extension().is_some() {
|
||||
@@ -55,13 +49,13 @@ fn any_handler(
|
||||
|
||||
return response;
|
||||
} else {
|
||||
path = website_path.join("index.html");
|
||||
path = dist_path.join("index.html");
|
||||
}
|
||||
}
|
||||
|
||||
path_to_response(&headers, &path)
|
||||
} else {
|
||||
path_to_response(&headers, &website_path.join("index.html"))
|
||||
path_to_response(&headers, &dist_path.join("index.html"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,49 +79,35 @@ fn path_to_response_(headers: &HeaderMap, path: &Path) -> color_eyre::Result<Res
|
||||
return Ok(Response::new_not_modified());
|
||||
}
|
||||
|
||||
let mut response;
|
||||
let content = fs::read(path).unwrap_or_else(|error| {
|
||||
error!("{error}");
|
||||
let path = path.to_str().unwrap();
|
||||
info!("Can't read file {path}");
|
||||
panic!("")
|
||||
});
|
||||
|
||||
let is_localhost = headers.check_if_host_is_localhost();
|
||||
|
||||
if !is_localhost
|
||||
&& path.extension().unwrap_or_else(|| {
|
||||
dbg!(path);
|
||||
panic!();
|
||||
}) == "js"
|
||||
{
|
||||
let content = minify_js(path);
|
||||
|
||||
response = Response::new(content.into());
|
||||
} else {
|
||||
let content = fs::read(path).unwrap_or_else(|error| {
|
||||
error!("{error}");
|
||||
let path = path.to_str().unwrap();
|
||||
info!("Can't read file {path}");
|
||||
panic!("")
|
||||
});
|
||||
|
||||
response = Response::new(content.into());
|
||||
}
|
||||
let mut response = Response::new(content.into());
|
||||
|
||||
let headers = response.headers_mut();
|
||||
headers.insert_cors();
|
||||
headers.insert_content_type(path);
|
||||
|
||||
if !is_localhost {
|
||||
let serialized_path = path.to_str().unwrap();
|
||||
let serialized_path = path.to_str().unwrap();
|
||||
|
||||
if serialized_path.contains("fonts/")
|
||||
|| serialized_path.contains("assets/")
|
||||
|| serialized_path.contains("packages/")
|
||||
|| path.extension().is_some_and(|extension| {
|
||||
extension == "pdf"
|
||||
|| extension == "jpg"
|
||||
|| extension == "png"
|
||||
|| extension == "woff2"
|
||||
})
|
||||
{
|
||||
headers.insert_cache_control_immutable();
|
||||
}
|
||||
if serialized_path.ends_with(".html") || serialized_path.ends_with("service-worker.js") {
|
||||
headers.insert_cache_control_must_revalidate();
|
||||
} else if serialized_path.contains("fonts/")
|
||||
|| serialized_path.contains("assets/")
|
||||
|| serialized_path.contains("packages/")
|
||||
|| path.extension().is_some_and(|extension| {
|
||||
extension == "pdf"
|
||||
|| extension == "jpg"
|
||||
|| extension == "png"
|
||||
|| extension == "woff2"
|
||||
|| extension == "js"
|
||||
})
|
||||
{
|
||||
headers.insert_cache_control_immutable();
|
||||
}
|
||||
|
||||
headers.insert_last_modified(date);
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// Source: https://github.com/oxc-project/oxc/blob/main/crates/oxc_minifier/examples/minifier.rs
|
||||
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use oxc::{
|
||||
allocator::Allocator,
|
||||
codegen::{Codegen, CodegenOptions, LegalComment},
|
||||
minifier::{CompressOptions, MangleOptions, Minifier, MinifierOptions},
|
||||
parser::Parser,
|
||||
span::SourceType,
|
||||
};
|
||||
|
||||
pub fn minify_js(path: &Path) -> String {
|
||||
let source_text = fs::read_to_string(path).unwrap();
|
||||
let source_type = SourceType::from_path(path).unwrap();
|
||||
|
||||
let allocator = Allocator::default();
|
||||
|
||||
let parser_return = Parser::new(&allocator, &source_text, source_type).parse();
|
||||
|
||||
let mut program = parser_return.program;
|
||||
|
||||
let minifier_return = Minifier::new(MinifierOptions {
|
||||
mangle: Some(MangleOptions::default()),
|
||||
compress: Some(CompressOptions::default()),
|
||||
})
|
||||
.build(&allocator, &mut program);
|
||||
|
||||
Codegen::new()
|
||||
.with_options(CodegenOptions {
|
||||
minify: true,
|
||||
single_quote: false,
|
||||
comments: false,
|
||||
annotation_comments: false,
|
||||
source_map_path: None,
|
||||
legal_comments: LegalComment::None,
|
||||
})
|
||||
.with_scoping(minifier_return.scoping)
|
||||
.build(&program)
|
||||
.code
|
||||
}
|
||||
@@ -3,7 +3,6 @@ use axum::{Router, routing::get};
|
||||
use super::AppState;
|
||||
|
||||
mod file;
|
||||
mod minify;
|
||||
mod website;
|
||||
|
||||
use file::{file_handler, index_handler};
|
||||
|
||||
@@ -19,6 +19,7 @@ use axum::{
|
||||
routing::get,
|
||||
serve,
|
||||
};
|
||||
use brk_bundler::bundle;
|
||||
use brk_computer::Computer;
|
||||
use brk_core::dot_brk_path;
|
||||
use brk_indexer::Indexer;
|
||||
@@ -45,6 +46,15 @@ pub struct AppState {
|
||||
websites_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn dist_path(&self) -> PathBuf {
|
||||
self.websites_path
|
||||
.as_ref()
|
||||
.expect("Should never reach here is websites_path is None")
|
||||
.join("dist")
|
||||
}
|
||||
}
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
const DEV_PATH: &str = "../..";
|
||||
@@ -103,9 +113,13 @@ impl Server {
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn serve(self) -> color_eyre::Result<()> {
|
||||
pub async fn serve(self, watch: bool) -> color_eyre::Result<()> {
|
||||
let state = self.0;
|
||||
|
||||
if let Some(websites_path) = state.websites_path.clone() {
|
||||
bundle(&websites_path, state.website.to_folder_name(), watch).await?;
|
||||
}
|
||||
|
||||
let compression_layer = CompressionLayer::new()
|
||||
.br(true)
|
||||
.deflate(true)
|
||||
|
||||
@@ -5,12 +5,11 @@ use std::{
|
||||
|
||||
use axum::http::{
|
||||
HeaderMap,
|
||||
header::{self, HOST, IF_MODIFIED_SINCE},
|
||||
header::{self, IF_MODIFIED_SINCE},
|
||||
};
|
||||
use jiff::{Timestamp, civil::DateTime, fmt::strtime, tz::TimeZone};
|
||||
use log::info;
|
||||
|
||||
const STALE_IF_ERROR: u64 = 30_000_000; // 1 Year ish
|
||||
const MODIFIED_SINCE_FORMAT: &str = "%a, %d %b %Y %H:%M:%S GMT";
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
@@ -20,12 +19,6 @@ pub enum ModifiedState {
|
||||
}
|
||||
|
||||
pub trait HeaderMapExtended {
|
||||
fn _get_scheme(&self) -> &str;
|
||||
fn get_host(&self) -> &str;
|
||||
fn check_if_host_is_local(&self) -> bool;
|
||||
fn check_if_host_is_0000(&self) -> bool;
|
||||
fn check_if_host_is_localhost(&self) -> bool;
|
||||
|
||||
fn insert_cors(&mut self);
|
||||
|
||||
fn get_if_modified_since(&self) -> Option<DateTime>;
|
||||
@@ -36,8 +29,8 @@ pub trait HeaderMapExtended {
|
||||
duration: Duration,
|
||||
) -> color_eyre::Result<(ModifiedState, DateTime)>;
|
||||
|
||||
fn insert_cache_control_must_revalidate(&mut self);
|
||||
fn insert_cache_control_immutable(&mut self);
|
||||
fn _insert_cache_control_revalidate(&mut self, max_age: u64, stale_while_revalidate: u64);
|
||||
fn insert_last_modified(&mut self, date: DateTime);
|
||||
|
||||
fn insert_content_disposition_attachment(&mut self);
|
||||
@@ -59,41 +52,22 @@ pub trait HeaderMapExtended {
|
||||
}
|
||||
|
||||
impl HeaderMapExtended for HeaderMap {
|
||||
fn _get_scheme(&self) -> &str {
|
||||
if self.check_if_host_is_local() {
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
}
|
||||
}
|
||||
|
||||
fn get_host(&self) -> &str {
|
||||
self[HOST].to_str().unwrap()
|
||||
}
|
||||
|
||||
fn check_if_host_is_local(&self) -> bool {
|
||||
self.check_if_host_is_localhost() || self.check_if_host_is_0000()
|
||||
}
|
||||
|
||||
fn check_if_host_is_0000(&self) -> bool {
|
||||
self.get_host().contains("0.0.0.0")
|
||||
}
|
||||
|
||||
fn check_if_host_is_localhost(&self) -> bool {
|
||||
self.get_host().contains("localhost")
|
||||
}
|
||||
|
||||
fn insert_cors(&mut self) {
|
||||
self.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap());
|
||||
self.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, "*".parse().unwrap());
|
||||
}
|
||||
|
||||
fn insert_cache_control_must_revalidate(&mut self) {
|
||||
self.insert(
|
||||
header::CACHE_CONTROL,
|
||||
"public, max-age=0, must-revalidate".parse().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
fn insert_cache_control_immutable(&mut self) {
|
||||
self.insert(
|
||||
header::CACHE_CONTROL,
|
||||
format!("public, max-age=604800, immutable, stale-if-error={STALE_IF_ERROR}")
|
||||
.parse()
|
||||
.unwrap(),
|
||||
"public, max-age=31536000, immutable".parse().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,16 +75,6 @@ impl HeaderMapExtended for HeaderMap {
|
||||
self.insert(header::CONTENT_DISPOSITION, "attachment".parse().unwrap());
|
||||
}
|
||||
|
||||
fn _insert_cache_control_revalidate(&mut self, max_age: u64, stale_while_revalidate: u64) {
|
||||
self.insert(
|
||||
header::CACHE_CONTROL,
|
||||
format!(
|
||||
"public, max-age={max_age}, stale-while-revalidate={stale_while_revalidate}, stale-if-error={STALE_IF_ERROR}")
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
fn insert_last_modified(&mut self, date: DateTime) {
|
||||
let formatted = date
|
||||
.to_zoned(TimeZone::system())
|
||||
@@ -167,7 +131,7 @@ impl HeaderMapExtended for HeaderMap {
|
||||
fn insert_content_type(&mut self, path: &Path) {
|
||||
match path.extension().unwrap().to_str().unwrap() {
|
||||
"js" => self.insert_content_type_application_javascript(),
|
||||
"json" => self.insert_content_type_application_json(),
|
||||
"json" | "map" => self.insert_content_type_application_json(),
|
||||
"html" => self.insert_content_type_text_html(),
|
||||
"css" => self.insert_content_type_text_css(),
|
||||
"toml" | "txt" => self.insert_content_type_text_plain(),
|
||||
|
||||
@@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
|
||||
Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, ValueEnum,
|
||||
)]
|
||||
pub enum Format {
|
||||
#[default]
|
||||
Compressed,
|
||||
#[default]
|
||||
Raw,
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ where
|
||||
}
|
||||
#[inline]
|
||||
fn get_or_read_(&self, index: usize, mmap: &Mmap) -> Result<Option<Value<T>>> {
|
||||
let stored_len = mmap.len() / Self::SIZE_OF_T;
|
||||
let stored_len = self.stored_len_(mmap);
|
||||
|
||||
if index >= stored_len {
|
||||
let pushed = self.pushed();
|
||||
@@ -53,6 +53,7 @@ where
|
||||
fn mmap(&self) -> &ArcSwap<Mmap>;
|
||||
|
||||
fn stored_len(&self) -> usize;
|
||||
fn stored_len_(&self, mmap: &Mmap) -> usize;
|
||||
|
||||
fn pushed(&self) -> &[T];
|
||||
#[inline]
|
||||
|
||||
@@ -21,7 +21,9 @@ use crate::{
|
||||
const ONE_KIB: usize = 1024;
|
||||
const ONE_MIB: usize = ONE_KIB * ONE_KIB;
|
||||
pub const MAX_CACHE_SIZE: usize = 100 * ONE_MIB;
|
||||
pub const MAX_PAGE_SIZE: usize = 16 * ONE_KIB;
|
||||
pub const MAX_PAGE_SIZE: usize = 64 * ONE_KIB;
|
||||
|
||||
const VERSION: Version = Version::ONE;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CompressedVec<I, T> {
|
||||
@@ -39,7 +41,9 @@ where
|
||||
pub const CACHE_LENGTH: usize = MAX_CACHE_SIZE / Self::PAGE_SIZE;
|
||||
|
||||
/// Same as import but will reset the folder under certain errors, so be careful !
|
||||
pub fn forced_import(path: &Path, version: Version) -> Result<Self> {
|
||||
pub fn forced_import(path: &Path, mut version: Version) -> Result<Self> {
|
||||
version = version + VERSION;
|
||||
|
||||
let res = Self::import(path, version);
|
||||
match res {
|
||||
Err(Error::WrongEndian)
|
||||
@@ -129,7 +133,7 @@ where
|
||||
page_index * Self::PER_PAGE
|
||||
}
|
||||
|
||||
fn stored_len_(pages_meta: &Guard<Arc<CompressedPagesMetadata>>) -> usize {
|
||||
fn stored_len__(pages_meta: &Guard<Arc<CompressedPagesMetadata>>) -> usize {
|
||||
if let Some(last) = pages_meta.last() {
|
||||
(pages_meta.len() - 1) * Self::PER_PAGE + last.values_len as usize
|
||||
} else {
|
||||
@@ -178,7 +182,11 @@ where
|
||||
|
||||
#[inline]
|
||||
fn stored_len(&self) -> usize {
|
||||
Self::stored_len_(&self.pages_meta.load())
|
||||
Self::stored_len__(&self.pages_meta.load())
|
||||
}
|
||||
#[inline]
|
||||
fn stored_len_(&self, _: &Mmap) -> usize {
|
||||
self.stored_len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -477,7 +485,7 @@ where
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let pages_meta = self.pages_meta.load();
|
||||
let stored_len = CompressedVec::<I, T>::stored_len_(&pages_meta);
|
||||
let stored_len = CompressedVec::<I, T>::stored_len__(&pages_meta);
|
||||
CompressedVecIterator {
|
||||
vec: self,
|
||||
guard: self.mmap().load(),
|
||||
|
||||
@@ -104,7 +104,11 @@ where
|
||||
|
||||
#[inline]
|
||||
fn stored_len(&self) -> usize {
|
||||
self.mmap.load().len() / Self::SIZE_OF_T
|
||||
self.stored_len_(&self.mmap.load())
|
||||
}
|
||||
#[inline]
|
||||
fn stored_len_(&self, mmap: &Mmap) -> usize {
|
||||
mmap.len() / Self::SIZE_OF_T
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -73,6 +73,13 @@ where
|
||||
StoredVec::Compressed(v) => v.stored_len(),
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
fn stored_len_(&self, mmap: &Mmap) -> usize {
|
||||
match self {
|
||||
StoredVec::Raw(v) => v.stored_len_(mmap),
|
||||
StoredVec::Compressed(v) => v.stored_len_(mmap),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn pushed(&self) -> &[T] {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
DATE=$(date -u '+%Y-%m-%d_%H-%M-%S')
|
||||
OUTPUT="/assets/pwa/${DATE}"
|
||||
|
||||
mkdir ".${OUTPUT}"
|
||||
cp "./assets/pwa/index.html" ".${OUTPUT}/"
|
||||
|
||||
pwa-asset-generator "../assets/dove-orange.svg" ".${OUTPUT}" \
|
||||
--index ".${OUTPUT}/index.html" \
|
||||
--manifest "./manifest.webmanifest" \
|
||||
--favicon \
|
||||
--padding "0%" \
|
||||
--path-override "${OUTPUT}" \
|
||||
--quality "100" \
|
||||
--opaque "false"
|
||||
|
||||
pwa-asset-generator "../assets/dove-white.svg" ".${OUTPUT}" \
|
||||
--index ".${OUTPUT}/index.html" \
|
||||
--manifest "./manifest.webmanifest" \
|
||||
--icon-only \
|
||||
--background "#f26610" \
|
||||
--padding "10%" \
|
||||
--path-override "${OUTPUT}" \
|
||||
--quality "100"
|
||||
|
||||
pwa-asset-generator "../assets/logo-stamp-orange.svg" ".${OUTPUT}" \
|
||||
--index ".${OUTPUT}/index.html" \
|
||||
--splash-only \
|
||||
--background "#f26610" \
|
||||
--padding "min(30vh, 30vw)" \
|
||||
--path-override "${OUTPUT}" \
|
||||
--quality "100"
|
||||
+270
-19
@@ -12,7 +12,7 @@
|
||||
/>
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<script type="module" crossorigin src="/scripts/main.js"></script>
|
||||
<script type="module" src="/scripts/main.js"></script>
|
||||
|
||||
<!-- ------ -->
|
||||
<!-- Styles -->
|
||||
@@ -281,7 +281,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: "Geist mono";
|
||||
src: url("./assets/fonts/geist_mono_var_1_4_01.woff2") format("woff2");
|
||||
src: url("./assets/fonts/geist_mono_var_v1_5_0.woff2") format("woff2");
|
||||
font-weight: 100 900;
|
||||
font-display: block;
|
||||
font-style: normal;
|
||||
@@ -965,7 +965,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin: -0.75rem var(--negative-main-padding);
|
||||
margin: -0.5rem var(--negative-main-padding);
|
||||
padding: 0.75rem var(--main-padding);
|
||||
overflow-x: auto;
|
||||
min-width: 0;
|
||||
@@ -1103,6 +1103,268 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#charts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
padding: var(--main-padding);
|
||||
|
||||
header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: -2rem;
|
||||
padding-left: var(--main-padding);
|
||||
margin-left: var(--negative-main-padding);
|
||||
padding-right: var(--main-padding);
|
||||
margin-right: var(--negative-main-padding);
|
||||
|
||||
& > * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
> .chart > legend,
|
||||
> fieldset {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.lightweight-chart {
|
||||
z-index: 40;
|
||||
}
|
||||
}
|
||||
|
||||
#table {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
padding: var(--main-padding);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
font-size: var(--font-size-xs);
|
||||
line--line-height: var(--line-height-xs);
|
||||
font-weight: 450;
|
||||
margin-left: var(--negative-main-padding);
|
||||
margin-right: var(--negative-main-padding);
|
||||
|
||||
table {
|
||||
z-index: 10;
|
||||
border-top-width: 1px;
|
||||
border-style: dashed !important;
|
||||
/* width: 100%; */
|
||||
line-height: var(--line-height-sm);
|
||||
text-transform: uppercase;
|
||||
table-layout: auto;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
/* border: 3px solid purple; */
|
||||
/* min-height: 100%; */
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
/* border-top: 1px; */
|
||||
border-right: 1px;
|
||||
border-bottom: 1px;
|
||||
border-color: var(--off-color);
|
||||
border-style: dashed !important;
|
||||
padding: 0.25rem 0.75rem;
|
||||
}
|
||||
|
||||
td {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
a {
|
||||
margin: -0.2rem 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
th:first-child {
|
||||
padding-left: var(--main-padding);
|
||||
}
|
||||
|
||||
th[scope="col"] {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--background-color);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 0.275rem;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
text-transform: lowercase;
|
||||
color: var(--off-color);
|
||||
text-align: left;
|
||||
|
||||
&:first-child {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
> span {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> button {
|
||||
padding: 0 0.25rem;
|
||||
margin: 0 -0.25rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
button:nth-of-type(1) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
button:nth-of-type(2) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
margin-right: -4px;
|
||||
/* width: 100%; */
|
||||
}
|
||||
|
||||
tbody {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
> button {
|
||||
padding: 1rem;
|
||||
min-width: 10rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
border-top-width: 1px;
|
||||
width: 100%;
|
||||
border-bottom-width: 1px;
|
||||
border-style: dashed !important;
|
||||
|
||||
> span {
|
||||
text-align: left;
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#simulation {
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
padding: var(--main-padding);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
overflow-y: auto;
|
||||
|
||||
> div:first-child {
|
||||
border-bottom: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex-direction: row;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: var(--bottom-area);
|
||||
}
|
||||
|
||||
> div:first-child {
|
||||
max-width: var(--default-main-width);
|
||||
border-right: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
overflow-x: hidden;
|
||||
|
||||
p {
|
||||
text-wrap: pretty;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
> span {
|
||||
display: block;
|
||||
}
|
||||
small {
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
flex: none;
|
||||
height: 400px;
|
||||
|
||||
.lightweight-chart {
|
||||
margin-left: calc(var(--negative-main-padding) * 0.75);
|
||||
|
||||
fieldset {
|
||||
margin-left: -0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- ------- -->
|
||||
@@ -1140,22 +1402,11 @@
|
||||
window.document.documentElement.dataset.display = "standalone";
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker
|
||||
.register("/service-worker.js")
|
||||
.then((registration) => {
|
||||
console.log(
|
||||
"Service Worker registered with scope:",
|
||||
registration.scope,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Service Worker registration failed:", error);
|
||||
});
|
||||
});
|
||||
// if ("serviceWorker" in navigator) {
|
||||
// navigator.serviceWorker.register("/service-worker.js");
|
||||
// }
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker.register("/service-worker.js");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- --- -->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kibo.money",
|
||||
"short_name": "kibo",
|
||||
"name": "brk",
|
||||
"short_name": "brk",
|
||||
"description": "A better, FOSS, Bitcoin-only, self-hostable Glassnode",
|
||||
"categories": [
|
||||
"bitcoin",
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
(async () => {
|
||||
const theme = await (
|
||||
await fetch(
|
||||
"https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/theme.css",
|
||||
)
|
||||
).text();
|
||||
|
||||
console.log(
|
||||
[
|
||||
"red",
|
||||
"orange",
|
||||
"amber",
|
||||
"yellow",
|
||||
"lime",
|
||||
"green",
|
||||
"emerald",
|
||||
"teal",
|
||||
"cyan",
|
||||
"sky",
|
||||
"blue",
|
||||
"indigo",
|
||||
"violet",
|
||||
"purple",
|
||||
"fuchsia",
|
||||
"pink",
|
||||
"rose",
|
||||
]
|
||||
.map((color) => {
|
||||
const [a, b] = [500, 600].map((shade) => {
|
||||
const regExp = new RegExp(
|
||||
`(?<=${`${color}-${shade}: oklch\(`})(.*?)(?=\\s*${`\);`})`,
|
||||
"g",
|
||||
);
|
||||
let res = regExp.exec(theme)?.[2];
|
||||
if (!res) throw "err";
|
||||
res = res.replace("(", "");
|
||||
res = res.replace(")", "");
|
||||
// return res
|
||||
return res.split(" ").map((s) => Number(s));
|
||||
});
|
||||
const mult = 10_000;
|
||||
return `--${color}: oklch(${[0, 1, 2].map((i) => Math.round(((a[i] + b[i]) / 2) * mult) / mult).join(" ")})`;
|
||||
})
|
||||
.join(";\n"),
|
||||
);
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
https://app.unpkg.com/@solidjs/signals@latest/files/dist/prod.js
|
||||
@@ -1,4 +0,0 @@
|
||||
import { Accessor, Setter } from "./v0.3.2-treeshaked/types/signals";
|
||||
|
||||
export type Signal<T> = Accessor<T> & { set: Setter<T>; reset: VoidFunction };
|
||||
export type Signals = Awaited<typeof import("./wrapper.js").default>;
|
||||
@@ -1,679 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
// src/core/error.ts
|
||||
var NotReadyError = class extends Error {};
|
||||
var EffectError = class extends Error {
|
||||
constructor(effect, cause) {
|
||||
super("");
|
||||
this.cause = cause;
|
||||
}
|
||||
};
|
||||
|
||||
// src/core/constants.ts
|
||||
var STATE_CLEAN = 0;
|
||||
var STATE_CHECK = 1;
|
||||
var STATE_DIRTY = 2;
|
||||
var STATE_DISPOSED = 3;
|
||||
var EFFECT_PURE = 0;
|
||||
var EFFECT_RENDER = 1;
|
||||
var EFFECT_USER = 2;
|
||||
|
||||
// src/core/scheduler.ts
|
||||
var clock = 0;
|
||||
function getClock() {
|
||||
return clock;
|
||||
}
|
||||
function incrementClock() {
|
||||
clock++;
|
||||
}
|
||||
var scheduled = false;
|
||||
function schedule() {
|
||||
if (scheduled) return;
|
||||
scheduled = true;
|
||||
if (!globalQueue.u) queueMicrotask(flushSync);
|
||||
}
|
||||
var pureQueue = [];
|
||||
var Queue = class {
|
||||
i = null;
|
||||
u = false;
|
||||
v = [[], []];
|
||||
t = [];
|
||||
created = clock;
|
||||
enqueue(type, fn) {
|
||||
pureQueue.push(fn);
|
||||
if (type) this.v[type - 1].push(fn);
|
||||
schedule();
|
||||
}
|
||||
run(type) {
|
||||
if (type === EFFECT_PURE) {
|
||||
pureQueue.length && runQueue(pureQueue, type);
|
||||
pureQueue = [];
|
||||
return;
|
||||
} else if (this.v[type - 1].length) {
|
||||
const effects = this.v[type - 1];
|
||||
this.v[type - 1] = [];
|
||||
runQueue(effects, type);
|
||||
}
|
||||
for (let i = 0; i < this.t.length; i++) {
|
||||
this.t[i].run(type);
|
||||
}
|
||||
}
|
||||
flush() {
|
||||
if (this.u) return;
|
||||
this.u = true;
|
||||
try {
|
||||
this.run(EFFECT_PURE);
|
||||
incrementClock();
|
||||
scheduled = false;
|
||||
this.run(EFFECT_RENDER);
|
||||
this.run(EFFECT_USER);
|
||||
} finally {
|
||||
this.u = false;
|
||||
}
|
||||
}
|
||||
addChild(child) {
|
||||
this.t.push(child);
|
||||
child.i = this;
|
||||
}
|
||||
removeChild(child) {
|
||||
const index = this.t.indexOf(child);
|
||||
if (index >= 0) this.t.splice(index, 1);
|
||||
}
|
||||
notify(...args) {
|
||||
if (this.i) return this.i.notify(...args);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
var globalQueue = new Queue();
|
||||
function flushSync() {
|
||||
while (scheduled) {
|
||||
globalQueue.flush();
|
||||
}
|
||||
}
|
||||
function runQueue(queue, type) {
|
||||
for (let i = 0; i < queue.length; i++) queue[i](type);
|
||||
}
|
||||
|
||||
// src/core/owner.ts
|
||||
var currentOwner = null;
|
||||
var defaultContext = {};
|
||||
function getOwner() {
|
||||
return currentOwner;
|
||||
}
|
||||
function setOwner(owner) {
|
||||
const out = currentOwner;
|
||||
currentOwner = owner;
|
||||
return out;
|
||||
}
|
||||
var Owner = class {
|
||||
// We flatten the owner tree into a linked list so that we don't need a pointer to .firstChild
|
||||
// However, the children are actually added in reverse creation order
|
||||
// See comment at the top of the file for an example of the _nextSibling traversal
|
||||
i = null;
|
||||
h = null;
|
||||
l = null;
|
||||
a = STATE_CLEAN;
|
||||
f = null;
|
||||
j = defaultContext;
|
||||
g = globalQueue;
|
||||
F = 0;
|
||||
id = null;
|
||||
constructor(id = null, skipAppend = false) {
|
||||
this.id = id;
|
||||
if (currentOwner) {
|
||||
!skipAppend && currentOwner.append(this);
|
||||
}
|
||||
}
|
||||
append(child) {
|
||||
child.i = this;
|
||||
child.l = this;
|
||||
if (this.h) this.h.l = child;
|
||||
child.h = this.h;
|
||||
this.h = child;
|
||||
if (this.id != null && child.id == null) child.id = this.getNextChildId();
|
||||
if (child.j !== this.j) {
|
||||
child.j = { ...this.j, ...child.j };
|
||||
}
|
||||
if (this.g) child.g = this.g;
|
||||
}
|
||||
dispose(self = true) {
|
||||
if (this.a === STATE_DISPOSED) return;
|
||||
let head = self ? this.l || this.i : this,
|
||||
current = this.h,
|
||||
next = null;
|
||||
while (current && current.i === this) {
|
||||
current.dispose(true);
|
||||
current.o();
|
||||
next = current.h;
|
||||
current.h = null;
|
||||
current = next;
|
||||
}
|
||||
this.F = 0;
|
||||
if (self) this.o();
|
||||
if (current) current.l = !self ? this : this.l;
|
||||
if (head) head.h = current;
|
||||
}
|
||||
o() {
|
||||
if (this.l) this.l.h = null;
|
||||
this.i = null;
|
||||
this.l = null;
|
||||
this.j = defaultContext;
|
||||
this.a = STATE_DISPOSED;
|
||||
this.emptyDisposal();
|
||||
}
|
||||
emptyDisposal() {
|
||||
if (!this.f) return;
|
||||
if (Array.isArray(this.f)) {
|
||||
for (let i = 0; i < this.f.length; i++) {
|
||||
const callable = this.f[i];
|
||||
callable.call(callable);
|
||||
}
|
||||
} else {
|
||||
this.f.call(this.f);
|
||||
}
|
||||
this.f = null;
|
||||
}
|
||||
getNextChildId() {
|
||||
if (this.id != null) return formatId(this.id, this.F++);
|
||||
throw new Error("Cannot get child id from owner without an id");
|
||||
}
|
||||
};
|
||||
function onCleanup(fn) {
|
||||
if (!currentOwner) return fn;
|
||||
const node = currentOwner;
|
||||
if (!node.f) {
|
||||
node.f = fn;
|
||||
} else if (Array.isArray(node.f)) {
|
||||
node.f.push(fn);
|
||||
} else {
|
||||
node.f = [node.f, fn];
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
function formatId(prefix, id) {
|
||||
const num = id.toString(36),
|
||||
len = num.length - 1;
|
||||
return prefix + (len ? String.fromCharCode(64 + len) : "") + num;
|
||||
}
|
||||
|
||||
// src/core/flags.ts
|
||||
var ERROR_OFFSET = 0;
|
||||
var ERROR_BIT = 1 << ERROR_OFFSET;
|
||||
var LOADING_OFFSET = 1;
|
||||
var LOADING_BIT = 1 << LOADING_OFFSET;
|
||||
var UNINITIALIZED_OFFSET = 2;
|
||||
var UNINITIALIZED_BIT = 1 << UNINITIALIZED_OFFSET;
|
||||
var DEFAULT_FLAGS = ERROR_BIT;
|
||||
|
||||
// src/core/core.ts
|
||||
var currentObserver = null;
|
||||
var currentMask = DEFAULT_FLAGS;
|
||||
var newSources = null;
|
||||
var newSourcesIndex = 0;
|
||||
var newFlags = 0;
|
||||
var notStale = false;
|
||||
var UNCHANGED = Symbol(0);
|
||||
var Computation = class extends Owner {
|
||||
b = null;
|
||||
c = null;
|
||||
e;
|
||||
w;
|
||||
p;
|
||||
// Used in __DEV__ mode, hopefully removed in production
|
||||
J;
|
||||
// Using false is an optimization as an alternative to _equals: () => false
|
||||
// which could enable more efficient DIRTY notification
|
||||
A = isEqual;
|
||||
G;
|
||||
/** Whether the computation is an error or has ancestors that are unresolved */
|
||||
d = 0;
|
||||
/** Which flags raised by sources are handled, vs. being passed through. */
|
||||
B = DEFAULT_FLAGS;
|
||||
q = -1;
|
||||
n = false;
|
||||
constructor(initialValue, compute2, options) {
|
||||
super(options?.id, compute2 === null);
|
||||
this.p = compute2;
|
||||
this.a = compute2 ? STATE_DIRTY : STATE_CLEAN;
|
||||
this.d = compute2 && initialValue === void 0 ? UNINITIALIZED_BIT : 0;
|
||||
this.e = initialValue;
|
||||
if (options?.equals !== void 0) this.A = options.equals;
|
||||
if (options?.unobserved) this.G = options?.unobserved;
|
||||
}
|
||||
H() {
|
||||
if (this.p) {
|
||||
if (this.d & ERROR_BIT && this.q <= getClock()) update(this);
|
||||
else this.r();
|
||||
}
|
||||
track(this);
|
||||
newFlags |= this.d & ~currentMask;
|
||||
if (this.d & ERROR_BIT) {
|
||||
throw this.w;
|
||||
} else {
|
||||
return this.e;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Return the current value of this computation
|
||||
* Automatically re-executes the surrounding computation when the value changes
|
||||
*/
|
||||
read() {
|
||||
return this.H();
|
||||
}
|
||||
/**
|
||||
* Return the current value of this computation
|
||||
* Automatically re-executes the surrounding computation when the value changes
|
||||
*
|
||||
* If the computation has any unresolved ancestors, this function waits for the value to resolve
|
||||
* before continuing
|
||||
*/
|
||||
wait() {
|
||||
if (this.p && this.d & ERROR_BIT && this.q <= getClock()) {
|
||||
update(this);
|
||||
} else {
|
||||
this.r();
|
||||
}
|
||||
track(this);
|
||||
if ((notStale || this.d & UNINITIALIZED_BIT) && this.d & LOADING_BIT) {
|
||||
throw new NotReadyError();
|
||||
}
|
||||
return this.H();
|
||||
}
|
||||
/** Update the computation with a new value. */
|
||||
write(value, flags = 0, raw = false) {
|
||||
const newValue =
|
||||
!raw && typeof value === "function" ? value(this.e) : value;
|
||||
const valueChanged =
|
||||
newValue !== UNCHANGED &&
|
||||
(!!(this.d & UNINITIALIZED_BIT) ||
|
||||
this.d & LOADING_BIT & ~flags ||
|
||||
this.A === false ||
|
||||
!this.A(this.e, newValue));
|
||||
if (valueChanged) {
|
||||
this.e = newValue;
|
||||
this.w = void 0;
|
||||
}
|
||||
const changedFlagsMask = this.d ^ flags,
|
||||
changedFlags = changedFlagsMask & flags;
|
||||
this.d = flags;
|
||||
this.q = getClock() + 1;
|
||||
if (this.c) {
|
||||
for (let i = 0; i < this.c.length; i++) {
|
||||
if (valueChanged) {
|
||||
this.c[i].k(STATE_DIRTY);
|
||||
} else if (changedFlagsMask) {
|
||||
this.c[i].I(changedFlagsMask, changedFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.e;
|
||||
}
|
||||
/**
|
||||
* Set the current node's state, and recursively mark all of this node's observers as STATE_CHECK
|
||||
*/
|
||||
k(state, skipQueue) {
|
||||
if (this.a >= state && !this.n) return;
|
||||
this.n = !!skipQueue;
|
||||
this.a = state;
|
||||
if (this.c) {
|
||||
for (let i = 0; i < this.c.length; i++) {
|
||||
this.c[i].k(STATE_CHECK, skipQueue);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Notify the computation that one of its sources has changed flags.
|
||||
*
|
||||
* @param mask A bitmask for which flag(s) were changed.
|
||||
* @param newFlags The source's new flags, masked to just the changed ones.
|
||||
*/
|
||||
I(mask, newFlags2) {
|
||||
if (this.a >= STATE_DIRTY) return;
|
||||
if (mask & this.B) {
|
||||
this.k(STATE_DIRTY);
|
||||
return;
|
||||
}
|
||||
if (this.a >= STATE_CHECK) return;
|
||||
const prevFlags = this.d & mask;
|
||||
const deltaFlags = prevFlags ^ newFlags2;
|
||||
if (newFlags2 === prevFlags);
|
||||
else if (deltaFlags & prevFlags & mask) {
|
||||
this.k(STATE_CHECK);
|
||||
} else {
|
||||
this.d ^= deltaFlags;
|
||||
if (this.c) {
|
||||
for (let i = 0; i < this.c.length; i++) {
|
||||
this.c[i].I(mask, newFlags2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
C(error) {
|
||||
this.w = error;
|
||||
this.write(
|
||||
UNCHANGED,
|
||||
(this.d & ~LOADING_BIT) | ERROR_BIT | UNINITIALIZED_BIT
|
||||
);
|
||||
}
|
||||
/**
|
||||
* This is the core part of the reactivity system, which makes sure that the values are updated
|
||||
* before they are read. We've also adapted it to return the loading state of the computation,
|
||||
* so that we can propagate that to the computation's observers.
|
||||
*
|
||||
* This function will ensure that the value and states we read from the computation are up to date
|
||||
*/
|
||||
r() {
|
||||
if (!this.p) {
|
||||
return;
|
||||
}
|
||||
if (this.a === STATE_DISPOSED) {
|
||||
return;
|
||||
}
|
||||
if (this.a === STATE_CLEAN) {
|
||||
return;
|
||||
}
|
||||
let observerFlags = 0;
|
||||
if (this.a === STATE_CHECK) {
|
||||
for (let i = 0; i < this.b.length; i++) {
|
||||
this.b[i].r();
|
||||
observerFlags |= this.b[i].d;
|
||||
if (this.a === STATE_DIRTY) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.a === STATE_DIRTY) {
|
||||
update(this);
|
||||
} else {
|
||||
this.write(UNCHANGED, observerFlags);
|
||||
this.a = STATE_CLEAN;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Remove ourselves from the owner graph and the computation graph
|
||||
*/
|
||||
o() {
|
||||
if (this.a === STATE_DISPOSED) return;
|
||||
if (this.b) removeSourceObservers(this, 0);
|
||||
super.o();
|
||||
}
|
||||
};
|
||||
function track(computation) {
|
||||
if (currentObserver) {
|
||||
if (
|
||||
!newSources &&
|
||||
currentObserver.b &&
|
||||
currentObserver.b[newSourcesIndex] === computation
|
||||
) {
|
||||
newSourcesIndex++;
|
||||
} else if (!newSources) newSources = [computation];
|
||||
else if (computation !== newSources[newSources.length - 1]) {
|
||||
newSources.push(computation);
|
||||
}
|
||||
}
|
||||
}
|
||||
function update(node) {
|
||||
const prevSources = newSources,
|
||||
prevSourcesIndex = newSourcesIndex,
|
||||
prevFlags = newFlags;
|
||||
newSources = null;
|
||||
newSourcesIndex = 0;
|
||||
newFlags = 0;
|
||||
try {
|
||||
node.dispose(false);
|
||||
node.emptyDisposal();
|
||||
const result = compute(node, node.p, node);
|
||||
node.write(result, newFlags, true);
|
||||
} catch (error) {
|
||||
if (error instanceof NotReadyError) {
|
||||
node.write(
|
||||
UNCHANGED,
|
||||
newFlags | LOADING_BIT | (node.d & UNINITIALIZED_BIT)
|
||||
);
|
||||
} else {
|
||||
node.C(error);
|
||||
}
|
||||
} finally {
|
||||
if (newSources) {
|
||||
if (node.b) removeSourceObservers(node, newSourcesIndex);
|
||||
if (node.b && newSourcesIndex > 0) {
|
||||
node.b.length = newSourcesIndex + newSources.length;
|
||||
for (let i = 0; i < newSources.length; i++) {
|
||||
node.b[newSourcesIndex + i] = newSources[i];
|
||||
}
|
||||
} else {
|
||||
node.b = newSources;
|
||||
}
|
||||
let source;
|
||||
for (let i = newSourcesIndex; i < node.b.length; i++) {
|
||||
source = node.b[i];
|
||||
if (!source.c) source.c = [node];
|
||||
else source.c.push(node);
|
||||
}
|
||||
} else if (node.b && newSourcesIndex < node.b.length) {
|
||||
removeSourceObservers(node, newSourcesIndex);
|
||||
node.b.length = newSourcesIndex;
|
||||
}
|
||||
newSources = prevSources;
|
||||
newSourcesIndex = prevSourcesIndex;
|
||||
newFlags = prevFlags;
|
||||
node.q = getClock() + 1;
|
||||
node.a = STATE_CLEAN;
|
||||
}
|
||||
}
|
||||
function removeSourceObservers(node, index) {
|
||||
let source;
|
||||
let swap;
|
||||
for (let i = index; i < node.b.length; i++) {
|
||||
source = node.b[i];
|
||||
if (source.c) {
|
||||
swap = source.c.indexOf(node);
|
||||
source.c[swap] = source.c[source.c.length - 1];
|
||||
source.c.pop();
|
||||
if (!source.c.length) source.G?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
function isEqual(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
function untrack(fn) {
|
||||
if (currentObserver === null) return fn();
|
||||
return compute(getOwner(), fn, null);
|
||||
}
|
||||
function latest(fn, fallback) {
|
||||
const argLength = arguments.length;
|
||||
const prevFlags = newFlags;
|
||||
const prevNotStale = notStale;
|
||||
notStale = false;
|
||||
try {
|
||||
return fn();
|
||||
} catch (err) {
|
||||
if (argLength > 1 && err instanceof NotReadyError) return fallback;
|
||||
throw err;
|
||||
} finally {
|
||||
newFlags = prevFlags;
|
||||
notStale = prevNotStale;
|
||||
}
|
||||
}
|
||||
function compute(owner, fn, observer) {
|
||||
const prevOwner = setOwner(owner),
|
||||
prevObserver = currentObserver,
|
||||
prevMask = currentMask,
|
||||
prevNotStale = notStale;
|
||||
currentObserver = observer;
|
||||
currentMask = observer?.B ?? DEFAULT_FLAGS;
|
||||
notStale = true;
|
||||
try {
|
||||
return fn(observer ? observer.e : void 0);
|
||||
} finally {
|
||||
setOwner(prevOwner);
|
||||
currentObserver = prevObserver;
|
||||
currentMask = prevMask;
|
||||
notStale = prevNotStale;
|
||||
}
|
||||
}
|
||||
|
||||
// src/core/effect.ts
|
||||
var Effect = class extends Computation {
|
||||
x;
|
||||
y;
|
||||
s;
|
||||
D = false;
|
||||
z;
|
||||
m;
|
||||
constructor(initialValue, compute2, effect, error, options) {
|
||||
super(initialValue, compute2, options);
|
||||
this.x = effect;
|
||||
this.y = error;
|
||||
this.z = initialValue;
|
||||
this.m = options?.render ? EFFECT_RENDER : EFFECT_USER;
|
||||
if (this.m === EFFECT_RENDER) {
|
||||
this.p = (p) =>
|
||||
getClock() > this.g.created && !(this.d & ERROR_BIT)
|
||||
? latest(() => compute2(p))
|
||||
: compute2(p);
|
||||
}
|
||||
this.r();
|
||||
!options?.defer &&
|
||||
(this.m === EFFECT_USER
|
||||
? this.g.enqueue(this.m, this.E.bind(this))
|
||||
: this.E(this.m));
|
||||
}
|
||||
write(value, flags = 0) {
|
||||
if (this.a == STATE_DIRTY) {
|
||||
this.d;
|
||||
this.d = flags;
|
||||
if (this.m === EFFECT_RENDER) {
|
||||
this.g.notify(this, LOADING_BIT | ERROR_BIT, flags);
|
||||
}
|
||||
}
|
||||
if (value === UNCHANGED) return this.e;
|
||||
this.e = value;
|
||||
this.D = true;
|
||||
return value;
|
||||
}
|
||||
k(state, skipQueue) {
|
||||
if (this.a >= state || skipQueue) return;
|
||||
if (this.a === STATE_CLEAN) this.g.enqueue(this.m, this.E.bind(this));
|
||||
this.a = state;
|
||||
}
|
||||
C(error) {
|
||||
this.w = error;
|
||||
this.s?.();
|
||||
this.g.notify(this, LOADING_BIT, 0);
|
||||
this.d = ERROR_BIT;
|
||||
if (this.m === EFFECT_USER) {
|
||||
try {
|
||||
return this.y
|
||||
? (this.s = this.y(error))
|
||||
: console.error(new EffectError(this.x, error));
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
if (!this.g.notify(this, ERROR_BIT, ERROR_BIT)) throw error;
|
||||
}
|
||||
o() {
|
||||
if (this.a === STATE_DISPOSED) return;
|
||||
this.x = void 0;
|
||||
this.z = void 0;
|
||||
this.y = void 0;
|
||||
this.s?.();
|
||||
this.s = void 0;
|
||||
super.o();
|
||||
}
|
||||
E(type) {
|
||||
if (type) {
|
||||
if (this.D && this.a !== STATE_DISPOSED) {
|
||||
this.s?.();
|
||||
try {
|
||||
this.s = this.x(this.e, this.z);
|
||||
} catch (e) {
|
||||
if (!this.g.notify(this, ERROR_BIT, ERROR_BIT)) throw e;
|
||||
} finally {
|
||||
this.z = this.e;
|
||||
this.D = false;
|
||||
}
|
||||
}
|
||||
} else this.a !== STATE_CLEAN && runTop(this);
|
||||
}
|
||||
};
|
||||
function runTop(node) {
|
||||
const ancestors = [];
|
||||
for (let current = node; current !== null; current = current.i) {
|
||||
if (current.a !== STATE_CLEAN) {
|
||||
ancestors.push(current);
|
||||
}
|
||||
}
|
||||
for (let i = ancestors.length - 1; i >= 0; i--) {
|
||||
if (ancestors[i].a !== STATE_DISPOSED) ancestors[i].r();
|
||||
}
|
||||
}
|
||||
|
||||
// src/signals.ts
|
||||
function createSignal(first, second, third) {
|
||||
if (typeof first === "function") {
|
||||
const memo = createMemo((p) => {
|
||||
const node2 = new Computation(
|
||||
first(p ? untrack(p[0]) : second),
|
||||
null,
|
||||
third
|
||||
);
|
||||
return [node2.read.bind(node2), node2.write.bind(node2)];
|
||||
});
|
||||
return [() => memo()[0](), (value) => memo()[1](value)];
|
||||
}
|
||||
const o = getOwner();
|
||||
const needsId = o?.id != null;
|
||||
const node = new Computation(
|
||||
first,
|
||||
null,
|
||||
needsId ? { id: o.getNextChildId(), ...second } : second
|
||||
);
|
||||
return [node.read.bind(node), node.write.bind(node)];
|
||||
}
|
||||
function createMemo(compute2, value, options) {
|
||||
let node = new Computation(value, compute2, options);
|
||||
let resolvedValue;
|
||||
return () => {
|
||||
if (node) {
|
||||
if (node.a === STATE_DISPOSED) {
|
||||
node = void 0;
|
||||
return resolvedValue;
|
||||
}
|
||||
resolvedValue = node.wait();
|
||||
if (!node.b?.length && node.h?.i !== node) {
|
||||
node.dispose();
|
||||
node = void 0;
|
||||
}
|
||||
}
|
||||
return resolvedValue;
|
||||
};
|
||||
}
|
||||
function createEffect(compute2, effect, error, value, options) {
|
||||
void new Effect(value, compute2, effect, error, options);
|
||||
}
|
||||
function createRoot(init, options) {
|
||||
const owner = new Owner(options?.id);
|
||||
return compute(
|
||||
owner,
|
||||
!init.length ? init : () => init(() => owner.dispose()),
|
||||
null
|
||||
);
|
||||
}
|
||||
function runWithOwner(owner, run) {
|
||||
return compute(owner, run, null);
|
||||
}
|
||||
|
||||
export {
|
||||
Owner,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createRoot,
|
||||
createSignal,
|
||||
getOwner,
|
||||
onCleanup,
|
||||
runWithOwner,
|
||||
untrack,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,153 +1,161 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @import { SignalOptions } from "./v0.3.2-treeshaked/types/core/core"
|
||||
* @import { getOwner as GetOwner, onCleanup as OnCleanup } from "./v0.3.2-treeshaked/types/core/owner"
|
||||
* @import { createSignal as CreateSignal, createEffect as CreateEffect, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner, Accessor } from "./v0.3.2-treeshaked/types/signals";
|
||||
* @import { Signal } from "./types";
|
||||
* @import { SignalOptions } from "./v0.3.2/types/core/core"
|
||||
* @import { getOwner as GetOwner, onCleanup as OnCleanup } from "./v0.3.2/types/core/owner"
|
||||
* @import { createSignal as CreateSignal, createEffect as CreateEffect, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner, Setter } from "./v0.3.2/types/signals";
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {() => T} Accessor
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Accessor<T> & { set: Setter<T>; reset: VoidFunction }} Signal
|
||||
*/
|
||||
|
||||
import {
|
||||
createSignal,
|
||||
createEffect,
|
||||
getOwner,
|
||||
createMemo,
|
||||
createRoot,
|
||||
runWithOwner,
|
||||
onCleanup,
|
||||
} from "./v0.3.2/script.js";
|
||||
|
||||
let effectCount = 0;
|
||||
|
||||
const importSignals = import("./v0.3.2-treeshaked/script.js").then(
|
||||
(_signals) => {
|
||||
const signals = {
|
||||
createSolidSignal: /** @type {typeof CreateSignal} */ (
|
||||
_signals.createSignal
|
||||
),
|
||||
createSolidEffect: /** @type {typeof CreateEffect} */ (
|
||||
_signals.createEffect
|
||||
),
|
||||
createEffect: /** @type {typeof CreateEffect} */ (
|
||||
// @ts-ignore
|
||||
(compute, effect) => {
|
||||
let dispose = /** @type {VoidFunction | null} */ (null);
|
||||
const signals = {
|
||||
createSolidSignal: /** @type {typeof CreateSignal} */ (createSignal),
|
||||
createSolidEffect: /** @type {typeof CreateEffect} */ (createEffect),
|
||||
createEffect: /** @type {typeof CreateEffect} */ (
|
||||
// @ts-ignore
|
||||
(compute, effect) => {
|
||||
let dispose = /** @type {VoidFunction | null} */ (null);
|
||||
|
||||
if (_signals.getOwner() === null) {
|
||||
throw Error("No owner");
|
||||
}
|
||||
if (getOwner() === null) {
|
||||
throw Error("No owner");
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (dispose) {
|
||||
dispose();
|
||||
dispose = null;
|
||||
// console.log("effectCount = ", --effectCount);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
_signals.createEffect(compute, (v, oldV) => {
|
||||
// console.log("effectCount = ", ++effectCount);
|
||||
cleanup();
|
||||
signals.createRoot((_dispose) => {
|
||||
dispose = _dispose;
|
||||
return effect(v, oldV);
|
||||
});
|
||||
signals.onCleanup(cleanup);
|
||||
});
|
||||
signals.onCleanup(cleanup);
|
||||
function cleanup() {
|
||||
if (dispose) {
|
||||
dispose();
|
||||
dispose = null;
|
||||
// console.log("effectCount = ", --effectCount);
|
||||
}
|
||||
),
|
||||
createMemo: /** @type {typeof CreateMemo} */ (_signals.createMemo),
|
||||
createRoot: /** @type {typeof CreateRoot} */ (_signals.createRoot),
|
||||
getOwner: /** @type {typeof GetOwner} */ (_signals.getOwner),
|
||||
runWithOwner: /** @type {typeof RunWithOwner} */ (_signals.runWithOwner),
|
||||
onCleanup: /** @type {typeof OnCleanup} */ (_signals.onCleanup),
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} initialValue
|
||||
* @param {SignalOptions<T> & {save?: {keyPrefix: string | Accessor<string>; key: string; serialize: (v: T) => string; deserialize: (v: string) => T; serializeParam?: boolean}}} [options]
|
||||
* @returns {Signal<T>}
|
||||
*/
|
||||
createSignal(initialValue, options) {
|
||||
const [get, set] = this.createSolidSignal(
|
||||
/** @type {any} */ (initialValue),
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
get.set = set;
|
||||
// @ts-ignore
|
||||
createEffect(compute, (v, oldV) => {
|
||||
// console.log("effectCount = ", ++effectCount);
|
||||
cleanup();
|
||||
signals.createRoot((_dispose) => {
|
||||
dispose = _dispose;
|
||||
return effect(v, oldV);
|
||||
});
|
||||
signals.onCleanup(cleanup);
|
||||
});
|
||||
signals.onCleanup(cleanup);
|
||||
}
|
||||
),
|
||||
createMemo: /** @type {typeof CreateMemo} */ (createMemo),
|
||||
createRoot: /** @type {typeof CreateRoot} */ (createRoot),
|
||||
getOwner: /** @type {typeof GetOwner} */ (getOwner),
|
||||
runWithOwner: /** @type {typeof RunWithOwner} */ (runWithOwner),
|
||||
onCleanup: /** @type {typeof OnCleanup} */ (onCleanup),
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} initialValue
|
||||
* @param {SignalOptions<T> & {save?: {keyPrefix: string | Accessor<string>; key: string; serialize: (v: T) => string; deserialize: (v: string) => T; serializeParam?: boolean}}} [options]
|
||||
* @returns {Signal<T>}
|
||||
*/
|
||||
createSignal(initialValue, options) {
|
||||
const [get, set] = this.createSolidSignal(
|
||||
/** @type {any} */ (initialValue),
|
||||
options,
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
get.reset = () => set(initialValue);
|
||||
// @ts-ignore
|
||||
get.set = set;
|
||||
|
||||
if (options?.save) {
|
||||
const save = options.save;
|
||||
// @ts-ignore
|
||||
get.reset = () => set(initialValue);
|
||||
|
||||
const paramKey = save.key;
|
||||
const storageKey = this.createMemo(
|
||||
() =>
|
||||
`${typeof save.keyPrefix === "string" ? save.keyPrefix : save.keyPrefix()}-${paramKey}`,
|
||||
);
|
||||
if (options?.save) {
|
||||
const save = options.save;
|
||||
|
||||
let serialized = /** @type {string | null} */ (null);
|
||||
if (options.save.serializeParam !== false) {
|
||||
serialized = new URLSearchParams(window.location.search).get(
|
||||
paramKey,
|
||||
);
|
||||
const paramKey = save.key;
|
||||
const storageKey = this.createMemo(
|
||||
() =>
|
||||
`${
|
||||
typeof save.keyPrefix === "string"
|
||||
? save.keyPrefix
|
||||
: save.keyPrefix()
|
||||
}-${paramKey}`,
|
||||
);
|
||||
|
||||
let serialized = /** @type {string | null} */ (null);
|
||||
if (options.save.serializeParam !== false) {
|
||||
serialized = new URLSearchParams(window.location.search).get(paramKey);
|
||||
}
|
||||
if (serialized === null) {
|
||||
serialized = localStorage.getItem(storageKey());
|
||||
}
|
||||
if (serialized) {
|
||||
set(() => (serialized ? save.deserialize(serialized) : initialValue));
|
||||
}
|
||||
|
||||
let firstRun1 = true;
|
||||
this.createEffect(storageKey, (storageKey) => {
|
||||
if (!firstRun1) {
|
||||
serialized = localStorage.getItem(storageKey);
|
||||
set(() => (serialized ? save.deserialize(serialized) : initialValue));
|
||||
}
|
||||
firstRun1 = false;
|
||||
});
|
||||
|
||||
let firstRun2 = true;
|
||||
this.createEffect(get, (value) => {
|
||||
if (!save) return;
|
||||
|
||||
if (!firstRun2) {
|
||||
if (
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
(initialValue === undefined ||
|
||||
initialValue === null ||
|
||||
save.serialize(value) !== save.serialize(initialValue))
|
||||
) {
|
||||
localStorage.setItem(storageKey(), save.serialize(value));
|
||||
} else {
|
||||
localStorage.removeItem(storageKey());
|
||||
}
|
||||
if (serialized === null) {
|
||||
serialized = localStorage.getItem(storageKey());
|
||||
}
|
||||
if (serialized) {
|
||||
set(() =>
|
||||
serialized ? save.deserialize(serialized) : initialValue,
|
||||
);
|
||||
}
|
||||
|
||||
let firstRun1 = true;
|
||||
this.createEffect(storageKey, (storageKey) => {
|
||||
if (!firstRun1) {
|
||||
serialized = localStorage.getItem(storageKey);
|
||||
set(() =>
|
||||
serialized ? save.deserialize(serialized) : initialValue,
|
||||
);
|
||||
}
|
||||
firstRun1 = false;
|
||||
});
|
||||
|
||||
let firstRun2 = true;
|
||||
this.createEffect(get, (value) => {
|
||||
if (!save) return;
|
||||
|
||||
if (!firstRun2) {
|
||||
if (
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
(initialValue === undefined ||
|
||||
initialValue === null ||
|
||||
save.serialize(value) !== save.serialize(initialValue))
|
||||
) {
|
||||
localStorage.setItem(storageKey(), save.serialize(value));
|
||||
} else {
|
||||
localStorage.removeItem(storageKey());
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
(initialValue === undefined ||
|
||||
initialValue === null ||
|
||||
save.serialize(value) !== save.serialize(initialValue))
|
||||
) {
|
||||
writeParam(paramKey, save.serialize(value));
|
||||
} else {
|
||||
removeParam(paramKey);
|
||||
}
|
||||
|
||||
firstRun2 = false;
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return get;
|
||||
},
|
||||
};
|
||||
if (
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
(initialValue === undefined ||
|
||||
initialValue === null ||
|
||||
save.serialize(value) !== save.serialize(initialValue))
|
||||
) {
|
||||
writeParam(paramKey, save.serialize(value));
|
||||
} else {
|
||||
removeParam(paramKey);
|
||||
}
|
||||
|
||||
return signals;
|
||||
firstRun2 = false;
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return get;
|
||||
},
|
||||
);
|
||||
};
|
||||
/** @typedef {typeof signals} Signals */
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
@@ -178,4 +186,4 @@ function removeParam(key) {
|
||||
writeParam(key, undefined);
|
||||
}
|
||||
|
||||
export default importSignals;
|
||||
export default signals;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
// @ts-check
|
||||
|
||||
const keyPrefix = "chart";
|
||||
const ONE_BTC_IN_SATS = 100_000_000;
|
||||
const AUTO = "auto";
|
||||
const LINE = "line";
|
||||
const CANDLE = "candle";
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Colors} args.colors
|
||||
* @param {LightweightCharts} args.lightweightCharts
|
||||
* @param {Accessor<ChartOption>} args.selected
|
||||
* @param {Accessor<ChartOption>} args.option
|
||||
* @param {Signals} args.signals
|
||||
* @param {Utilities} args.utils
|
||||
* @param {WebSockets} args.webSockets
|
||||
@@ -18,7 +22,7 @@ export function init({
|
||||
colors,
|
||||
elements,
|
||||
lightweightCharts,
|
||||
selected,
|
||||
option,
|
||||
signals,
|
||||
utils,
|
||||
webSockets,
|
||||
@@ -31,10 +35,15 @@ export function init({
|
||||
const { headerElement, headingElement } = utils.dom.createHeader();
|
||||
elements.charts.append(headerElement);
|
||||
|
||||
const { index, fieldset } = createIndexSelector({ signals, utils });
|
||||
const { index, fieldset } = createIndexSelector({
|
||||
option,
|
||||
vecIdToIndexes,
|
||||
signals,
|
||||
utils,
|
||||
});
|
||||
|
||||
const TIMERANGE_LS_KEY = signals.createMemo(
|
||||
() => `chart-timerange-${index()}`,
|
||||
() => `chart-timerange-${index()}`
|
||||
);
|
||||
|
||||
let firstRun = true;
|
||||
@@ -57,7 +66,6 @@ export function init({
|
||||
});
|
||||
|
||||
const chart = lightweightCharts.createChartElement({
|
||||
owner: signals.getOwner(),
|
||||
parent: elements.charts,
|
||||
signals,
|
||||
colors,
|
||||
@@ -89,20 +97,37 @@ export function init({
|
||||
from.set(t.from);
|
||||
to.set(t.to);
|
||||
}
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
elements.charts.append(fieldset);
|
||||
|
||||
const { field: seriesTypeField, selected: topSeriesType } =
|
||||
const { field: seriesTypeField, selected: topSeriesType_ } =
|
||||
utils.dom.createHorizontalChoiceField({
|
||||
defaultValue: "Line",
|
||||
defaultValue: CANDLE,
|
||||
keyPrefix,
|
||||
key: "seriestype-0",
|
||||
choices: /** @type {const} */ (["Candles", "Line"]),
|
||||
choices: /** @type {const} */ ([AUTO, CANDLE, LINE]),
|
||||
signals,
|
||||
});
|
||||
|
||||
const topSeriesType = signals.createMemo(() => {
|
||||
const topSeriesType = topSeriesType_();
|
||||
if (topSeriesType === AUTO) {
|
||||
const t = to();
|
||||
const f = from();
|
||||
if (!t || !f) return null;
|
||||
const diff = t - f;
|
||||
if (diff / chart.inner.paneSize().width <= 0.5) {
|
||||
return CANDLE;
|
||||
} else {
|
||||
return LINE;
|
||||
}
|
||||
} else {
|
||||
return topSeriesType;
|
||||
}
|
||||
});
|
||||
|
||||
const { field: topUnitField, selected: topUnit } =
|
||||
utils.dom.createHorizontalChoiceField({
|
||||
defaultValue: "USD",
|
||||
@@ -128,7 +153,89 @@ export function init({
|
||||
const seriesListTop = /** @type {Series[]} */ ([]);
|
||||
const seriesListBottom = /** @type {Series[]} */ ([]);
|
||||
|
||||
signals.createEffect(selected, (option) => {
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {ISeries} params.iseries
|
||||
* @param {Unit} params.unit
|
||||
* @param {Index} params.index
|
||||
*/
|
||||
function printLatest({ iseries, unit, index }) {
|
||||
const _latest = webSockets.kraken1dCandle.latest();
|
||||
|
||||
if (!_latest) return;
|
||||
|
||||
const latest = { ..._latest };
|
||||
|
||||
if (unit === "Sats") {
|
||||
latest.open = Math.floor(ONE_BTC_IN_SATS / latest.open);
|
||||
latest.high = Math.floor(ONE_BTC_IN_SATS / latest.high);
|
||||
latest.low = Math.floor(ONE_BTC_IN_SATS / latest.low);
|
||||
latest.close = Math.floor(ONE_BTC_IN_SATS / latest.close);
|
||||
latest.value = Math.floor(ONE_BTC_IN_SATS / latest.value);
|
||||
}
|
||||
|
||||
const last_ = iseries.data().at(-1);
|
||||
if (!last_) return;
|
||||
const last = { ...last_ };
|
||||
|
||||
if ("close" in last) {
|
||||
last.close = latest.close;
|
||||
}
|
||||
if ("value" in last) {
|
||||
last.value = latest.value;
|
||||
}
|
||||
const date = new Date(latest.time * 1000);
|
||||
|
||||
switch (index) {
|
||||
case /** @satisfies {Height} */ (5): {
|
||||
if ("close" in last) {
|
||||
last.low = Math.min(last.low, latest.close);
|
||||
last.high = Math.max(last.high, latest.close);
|
||||
}
|
||||
iseries.update(last);
|
||||
break;
|
||||
}
|
||||
case /** @satisfies {DateIndex} */ (0): {
|
||||
iseries.update(latest);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (index === /** @satisfies {WeekIndex} */ (22)) {
|
||||
date.setUTCDate(date.getUTCDate() - ((date.getUTCDay() + 6) % 7));
|
||||
} else if (index === /** @satisfies {MonthIndex} */ (7)) {
|
||||
date.setUTCDate(1);
|
||||
} else if (index === /** @satisfies {QuarterIndex} */ (19)) {
|
||||
const month = date.getUTCMonth();
|
||||
date.setUTCMonth(month - (month % 3), 1);
|
||||
} else if (index === /** @satisfies {YearIndex} */ (23)) {
|
||||
date.setUTCMonth(0, 1);
|
||||
} else if (index === /** @satisfies {DecadeIndex} */ (1)) {
|
||||
date.setUTCFullYear(
|
||||
Math.floor(date.getUTCFullYear() / 10) * 10,
|
||||
0,
|
||||
1
|
||||
);
|
||||
} else {
|
||||
throw Error("Unsupported");
|
||||
}
|
||||
|
||||
const time = date.valueOf() / 1000;
|
||||
|
||||
if (time === last.time) {
|
||||
if ("close" in last) {
|
||||
last.low = Math.min(last.low, latest.low);
|
||||
last.high = Math.max(last.high, latest.high);
|
||||
}
|
||||
iseries.update(last);
|
||||
} else {
|
||||
latest.time = time;
|
||||
iseries.update(latest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signals.createEffect(option, (option) => {
|
||||
headingElement.innerHTML = option.title;
|
||||
|
||||
const bottomUnits = /** @type {readonly Unit[]} */ (
|
||||
@@ -166,80 +273,92 @@ export function init({
|
||||
|
||||
signals.createEffect(index, (index) => {
|
||||
signals.createEffect(
|
||||
() => [topUnit(), topSeriesType()],
|
||||
([topUnit, topSeriesType]) => {
|
||||
() => ({
|
||||
topUnit: topUnit(),
|
||||
topSeriesType: topSeriesType(),
|
||||
}),
|
||||
({ topUnit, topSeriesType }) => {
|
||||
/** @type {Series | undefined} */
|
||||
let series;
|
||||
|
||||
switch (topUnit) {
|
||||
case "USD": {
|
||||
switch (topSeriesType) {
|
||||
case "Candles": {
|
||||
const series = chart.addCandlestickSeries({
|
||||
case CANDLE: {
|
||||
series = chart.addCandlestickSeries({
|
||||
vecId: "ohlc",
|
||||
name: "Price",
|
||||
unit: topUnit,
|
||||
setDataCallback: printLatest,
|
||||
order: 0,
|
||||
});
|
||||
seriesListTop[0]?.remove();
|
||||
seriesListTop[0] = series;
|
||||
|
||||
break;
|
||||
}
|
||||
case "Line": {
|
||||
const series = chart.addLineSeries({
|
||||
case LINE: {
|
||||
series = chart.addLineSeries({
|
||||
vecId: "close",
|
||||
name: "Price",
|
||||
unit: topUnit,
|
||||
color: colors.default,
|
||||
setDataCallback: printLatest,
|
||||
options: {
|
||||
priceLineVisible: true,
|
||||
},
|
||||
order: 0,
|
||||
});
|
||||
seriesListTop[0]?.remove();
|
||||
seriesListTop[0] = series;
|
||||
}
|
||||
}
|
||||
// signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
|
||||
// if (!latest) return;
|
||||
// const last = /** @type { CandlestickData | undefined} */ (
|
||||
// candles.data().at(-1)
|
||||
// );
|
||||
// if (!last) return;
|
||||
// candles?.update({ ...last, close: latest.close });
|
||||
// });
|
||||
break;
|
||||
}
|
||||
case "Sats": {
|
||||
switch (topSeriesType) {
|
||||
case "Candles": {
|
||||
const series = chart.addCandlestickSeries({
|
||||
case CANDLE: {
|
||||
series = chart.addCandlestickSeries({
|
||||
vecId: "ohlc-in-sats",
|
||||
name: "Price",
|
||||
unit: topUnit,
|
||||
inverse: true,
|
||||
setDataCallback: printLatest,
|
||||
order: 0,
|
||||
});
|
||||
seriesListTop[0]?.remove();
|
||||
seriesListTop[0] = series;
|
||||
break;
|
||||
}
|
||||
case "Line": {
|
||||
const series = chart.addLineSeries({
|
||||
case LINE: {
|
||||
series = chart.addLineSeries({
|
||||
vecId: "close-in-sats",
|
||||
name: "Price",
|
||||
unit: topUnit,
|
||||
color: colors.default,
|
||||
setDataCallback: printLatest,
|
||||
options: {
|
||||
priceLineVisible: true,
|
||||
},
|
||||
order: 0,
|
||||
});
|
||||
seriesListTop[0]?.remove();
|
||||
seriesListTop[0] = series;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
if (!series) throw Error("Unreachable");
|
||||
|
||||
seriesListTop[0]?.remove();
|
||||
seriesListTop[0] = series;
|
||||
|
||||
// setDataCallback insimport("./options").tead of hasData
|
||||
signals.createEffect(
|
||||
() => ({
|
||||
latest: webSockets.kraken1dCandle.latest(),
|
||||
hasData: series.hasData(),
|
||||
}),
|
||||
({ latest, hasData }) => {
|
||||
if (!series || !latest || !hasData) return;
|
||||
printLatest({ iseries: series.inner, unit: topUnit, index });
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
[
|
||||
@@ -300,7 +419,7 @@ export function init({
|
||||
blueprint.color?.() ?? blueprint.colors?.[1](),
|
||||
},
|
||||
order,
|
||||
}),
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -318,13 +437,13 @@ export function init({
|
||||
paneIndex,
|
||||
options: blueprint.options,
|
||||
order,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
firstRun = false;
|
||||
@@ -334,25 +453,53 @@ export function init({
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Accessor<ChartOption>} args.option
|
||||
* @param {VecIdToIndexes} args.vecIdToIndexes
|
||||
* @param {Signals} args.signals
|
||||
* @param {Utilities} args.utils
|
||||
*/
|
||||
function createIndexSelector({ signals, utils }) {
|
||||
function createIndexSelector({ option, vecIdToIndexes, signals, utils }) {
|
||||
const choices_ = /** @type {const} */ ([
|
||||
"timestamp",
|
||||
"date",
|
||||
"week",
|
||||
// "difficulty epoch",
|
||||
"month",
|
||||
"quarter",
|
||||
"year",
|
||||
// "halving epoch",
|
||||
"decade",
|
||||
]);
|
||||
|
||||
/** @type {Accessor<typeof choices_>} */
|
||||
const choices = signals.createMemo(() => {
|
||||
const o = option();
|
||||
|
||||
if (!Object.keys(o.top).length && !Object.keys(o.bottom).length) {
|
||||
return [...choices_];
|
||||
}
|
||||
const rawIndexes = new Set(
|
||||
[Object.values(o.top), Object.values(o.bottom)]
|
||||
.flat(2)
|
||||
.map((blueprint) => vecIdToIndexes[blueprint.key])
|
||||
.flat()
|
||||
);
|
||||
|
||||
const serializedIndexes = [...rawIndexes].flatMap((index) => {
|
||||
const c = utils.serde.chartableIndex.serialize(index);
|
||||
return c ? [c] : [];
|
||||
});
|
||||
|
||||
return /** @type {any} */ (
|
||||
choices_.filter((choice) => serializedIndexes.includes(choice))
|
||||
);
|
||||
});
|
||||
|
||||
const { field, selected } = utils.dom.createHorizontalChoiceField({
|
||||
defaultValue: "date",
|
||||
keyPrefix,
|
||||
key: "index",
|
||||
choices: /**@type {const} */ ([
|
||||
"timestamp",
|
||||
"date",
|
||||
"week",
|
||||
// "difficulty epoch",
|
||||
"month",
|
||||
"quarter",
|
||||
"year",
|
||||
// "halving epoch",
|
||||
"decade",
|
||||
]),
|
||||
choices,
|
||||
id: "index",
|
||||
signals,
|
||||
});
|
||||
@@ -361,25 +508,8 @@ function createIndexSelector({ signals, utils }) {
|
||||
fieldset.append(field);
|
||||
fieldset.dataset.size = "sm";
|
||||
|
||||
const index = signals.createMemo(
|
||||
/** @returns {ChartableIndex} */ () => {
|
||||
switch (selected()) {
|
||||
case "timestamp":
|
||||
return /** @satisfies {Height} */ (5);
|
||||
case "date":
|
||||
return /** @satisfies {DateIndex} */ (0);
|
||||
case "week":
|
||||
return /** @satisfies {WeekIndex} */ (22);
|
||||
case "month":
|
||||
return /** @satisfies {MonthIndex} */ (7);
|
||||
case "quarter":
|
||||
return /** @satisfies {QuarterIndex} */ (19);
|
||||
case "year":
|
||||
return /** @satisfies {YearIndex} */ (23);
|
||||
case "decade":
|
||||
return /** @satisfies {DecadeIndex} */ (1);
|
||||
}
|
||||
},
|
||||
const index = signals.createMemo(() =>
|
||||
utils.serde.chartableIndex.deserialize(selected())
|
||||
);
|
||||
|
||||
return { fieldset, index };
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// DO NOT CHANGE, Exact format is expected in `brk_bundler`
|
||||
// @ts-ignore
|
||||
import("./main.js");
|
||||
+214
-181
@@ -1,14 +1,10 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, ChartableIndex,CreatePriceLineOptions, CreatePriceLine } from "./options"
|
||||
* @import { Valued, SingleValueData, CandlestickData, ChartData, OHLCTuple, Series } from "../packages/lightweight-charts/wrapper"
|
||||
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, ChartableIndex, SeriesType } from "./options"
|
||||
* @import { Valued, SingleValueData, CandlestickData, ChartData, OHLCTuple, Series, ISeries, LineData, BaselineData, PartialLineStyleOptions, PartialBaselineStyleOptions, PartialCandlestickStyleOptions } from "../packages/lightweight-charts/wrapper"
|
||||
* @import * as _ from "../packages/ufuzzy/v1.0.18/types"
|
||||
* @import { createChart as CreateClassicChart, LineStyleOptions, DeepPartial, ChartOptions, IChartApi, IHorzScaleBehavior, WhitespaceData, ISeriesApi, Time, LineData, LogicalRange, BaselineStyleOptions, SeriesOptionsCommon, BaselineData, CandlestickStyleOptions } from "../packages/lightweight-charts/v5.0.7-treeshaked/types"
|
||||
* @import { SignalOptions } from "../packages/solid-signals/v0.3.2-treeshaked/types/core/core"
|
||||
* @import {Signal, Signals} from "../packages/solid-signals/types";
|
||||
* @import { getOwner as GetOwner, onCleanup as OnCleanup, Owner } from "../packages/solid-signals/v0.3.2-treeshaked/types/core/owner"
|
||||
* @import { createEffect as CreateEffect, Accessor, Setter, createMemo as CreateMemo } from "../packages/solid-signals/v0.3.2-treeshaked/types/signals";
|
||||
* @import { Signal, Signals, Accessor } from "../packages/solid-signals/wrapper";
|
||||
* @import { DateIndex, DecadeIndex, DifficultyEpoch, Index, HalvingEpoch, Height, MonthIndex, P2PK33Index, P2PK65Index, P2PKHIndex, P2SHIndex, P2MSIndex, P2AIndex, P2TRIndex, P2WPKHIndex, P2WSHIndex, TxIndex, InputIndex, OutputIndex, VecId, WeekIndex, YearIndex, VecIdToIndexes, QuarterIndex, EmptyOutputIndex, OpReturnIndex, UnknownOutputIndex } from "./vecid-to-indexes"
|
||||
*/
|
||||
|
||||
@@ -66,14 +62,14 @@ const localhost = window.location.hostname === "localhost";
|
||||
function initPackages() {
|
||||
const imports = {
|
||||
async signals() {
|
||||
return import("../packages/solid-signals/wrapper.js").then((d) =>
|
||||
d.default.then((d) => d),
|
||||
return import("../packages/solid-signals/wrapper.js").then(
|
||||
(d) => d.default,
|
||||
);
|
||||
},
|
||||
async lightweightCharts() {
|
||||
return window.document.fonts.ready.then(() =>
|
||||
import("../packages/lightweight-charts/wrapper.js").then((d) =>
|
||||
d.default.then((d) => d),
|
||||
import("../packages/lightweight-charts/wrapper.js").then(
|
||||
(d) => d.default,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -350,23 +346,16 @@ function createUtils() {
|
||||
head.appendChild(link);
|
||||
return link;
|
||||
},
|
||||
/**
|
||||
* @param {string} href
|
||||
* @param {VoidFunction} callback
|
||||
*/
|
||||
importStyleAndThen(href, callback) {
|
||||
this.importStyle(href).addEventListener("load", callback);
|
||||
},
|
||||
/**
|
||||
* @template {Readonly<string[]>} T
|
||||
* @param {Object} args
|
||||
* @param {T[number]} args.defaultValue
|
||||
* @param {string} [args.id]
|
||||
* @param {T} args.choices
|
||||
* @param {T | Accessor<T>} args.choices
|
||||
* @param {string} [args.keyPrefix]
|
||||
* @param {string} args.key
|
||||
* @param {boolean} [args.sorted]
|
||||
* @param {{createEffect: CreateEffect, createSignal: Signals["createSignal"]}} args.signals
|
||||
* @param {Signals} args.signals
|
||||
*/
|
||||
createHorizontalChoiceField({
|
||||
id,
|
||||
@@ -377,13 +366,21 @@ function createUtils() {
|
||||
signals,
|
||||
sorted,
|
||||
}) {
|
||||
const choices = sorted
|
||||
? /** @type {T} */ (
|
||||
/** @type {any} */ (
|
||||
unsortedChoices.toSorted((a, b) => a.localeCompare(b))
|
||||
const choices = signals.createMemo(() => {
|
||||
/** @type {T} */
|
||||
let c;
|
||||
if (typeof unsortedChoices === "function") {
|
||||
c = unsortedChoices();
|
||||
} else {
|
||||
c = unsortedChoices;
|
||||
}
|
||||
|
||||
return sorted
|
||||
? /** @type {T} */ (
|
||||
/** @type {any} */ (c.toSorted((a, b) => a.localeCompare(b)))
|
||||
)
|
||||
)
|
||||
: unsortedChoices;
|
||||
: c;
|
||||
});
|
||||
|
||||
/** @type {Signal<T[number]>} */
|
||||
const selected = signals.createSignal(defaultValue, {
|
||||
@@ -394,31 +391,39 @@ function createUtils() {
|
||||
},
|
||||
});
|
||||
|
||||
if (!choices.includes(selected())) {
|
||||
console.log(choices, "don't include", selected());
|
||||
selected.set(() => defaultValue);
|
||||
}
|
||||
|
||||
const field = window.document.createElement("div");
|
||||
field.classList.add("field");
|
||||
|
||||
const div = window.document.createElement("div");
|
||||
field.append(div);
|
||||
|
||||
choices.forEach((choice) => {
|
||||
const inputValue = choice;
|
||||
const { label } = this.createLabeledInput({
|
||||
inputId: `${id ?? key}-${choice.toLowerCase()}`,
|
||||
inputName: id ?? key,
|
||||
inputValue,
|
||||
inputChecked: inputValue === selected(),
|
||||
labelTitle: choice,
|
||||
type: "radio",
|
||||
});
|
||||
signals.createEffect(choices, (choices) => {
|
||||
const s = selected();
|
||||
if (!choices.includes(s)) {
|
||||
if (choices.includes(defaultValue)) {
|
||||
selected.set(() => defaultValue);
|
||||
} else if (choices.length) {
|
||||
selected.set(() => choices[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const text = window.document.createTextNode(choice);
|
||||
label.append(text);
|
||||
div.append(label);
|
||||
div.innerHTML = "";
|
||||
|
||||
choices.forEach((choice) => {
|
||||
const inputValue = choice;
|
||||
const { label } = this.createLabeledInput({
|
||||
inputId: `${id ?? key}-${choice.toLowerCase()}`,
|
||||
inputName: id ?? key,
|
||||
inputValue,
|
||||
inputChecked: inputValue === selected(),
|
||||
labelTitle: choice,
|
||||
type: "radio",
|
||||
});
|
||||
|
||||
const text = window.document.createTextNode(choice);
|
||||
label.append(text);
|
||||
div.append(label);
|
||||
});
|
||||
});
|
||||
|
||||
field.addEventListener("change", (event) => {
|
||||
@@ -1093,6 +1098,116 @@ function createUtils() {
|
||||
}
|
||||
},
|
||||
},
|
||||
index: {
|
||||
/**
|
||||
* @param {Index} v
|
||||
*/
|
||||
serialize(v) {
|
||||
switch (v) {
|
||||
case /** @satisfies {DateIndex} */ (0):
|
||||
return "dateindex";
|
||||
case /** @satisfies {DecadeIndex} */ (1):
|
||||
return "decadeindex";
|
||||
case /** @satisfies {DifficultyEpoch} */ (2):
|
||||
return "difficultyepoch";
|
||||
case /** @satisfies {EmptyOutputIndex} */ (3):
|
||||
return "emptyoutputindex";
|
||||
case /** @satisfies {HalvingEpoch} */ (4):
|
||||
return "halvingepoch";
|
||||
case /** @satisfies {Height} */ (5):
|
||||
return "height";
|
||||
case /** @satisfies {InputIndex} */ (6):
|
||||
return "inputindex";
|
||||
case /** @satisfies {MonthIndex} */ (7):
|
||||
return "monthindex";
|
||||
case /** @satisfies {OpReturnIndex} */ (8):
|
||||
return "opreturnindex";
|
||||
case /** @satisfies {OutputIndex} */ (9):
|
||||
return "outputindex";
|
||||
case /** @satisfies {P2AIndex} */ (10):
|
||||
return "p2aindex";
|
||||
case /** @satisfies {P2MSIndex} */ (11):
|
||||
return "p2msindex";
|
||||
case /** @satisfies {P2PK33Index} */ (12):
|
||||
return "p2pk33index";
|
||||
case /** @satisfies {P2PK65Index} */ (13):
|
||||
return "p2pk65index";
|
||||
case /** @satisfies {P2PKHIndex} */ (14):
|
||||
return "p2pkhindex";
|
||||
case /** @satisfies {P2SHIndex} */ (15):
|
||||
return "p2shindex";
|
||||
case /** @satisfies {P2TRIndex} */ (16):
|
||||
return "p2trindex";
|
||||
case /** @satisfies {P2WPKHIndex} */ (17):
|
||||
return "p2wpkhindex";
|
||||
case /** @satisfies {P2WSHIndex} */ (18):
|
||||
return "p2wshindex";
|
||||
case /** @satisfies {QuarterIndex} */ (19):
|
||||
return "quarterindex";
|
||||
case /** @satisfies {TxIndex} */ (20):
|
||||
return "txindex";
|
||||
case /** @satisfies {UnknownOutputIndex} */ (21):
|
||||
return "unknownoutputindex";
|
||||
case /** @satisfies {WeekIndex} */ (22):
|
||||
return "weekindex";
|
||||
case /** @satisfies {YearIndex} */ (23):
|
||||
return "yearindex";
|
||||
}
|
||||
},
|
||||
},
|
||||
chartableIndex: {
|
||||
/**
|
||||
* @param {Index} v
|
||||
*/
|
||||
serialize(v) {
|
||||
switch (v) {
|
||||
case /** @satisfies {DateIndex} */ (0):
|
||||
return "date";
|
||||
case /** @satisfies {DecadeIndex} */ (1):
|
||||
return "decade";
|
||||
// case /** @satisfies {DifficultyEpoch} */ (2):
|
||||
// return "difficulty";
|
||||
// case /** @satisfies {HalvingEpoch} */ (4):
|
||||
// return "halving";
|
||||
case /** @satisfies {Height} */ (5):
|
||||
return "timestamp";
|
||||
case /** @satisfies {MonthIndex} */ (7):
|
||||
return "month";
|
||||
case /** @satisfies {QuarterIndex} */ (19):
|
||||
return "quarter";
|
||||
case /** @satisfies {WeekIndex} */ (22):
|
||||
return "week";
|
||||
case /** @satisfies {YearIndex} */ (23):
|
||||
return "year";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
* @returns {ChartableIndex}
|
||||
*/
|
||||
deserialize(v) {
|
||||
switch (v) {
|
||||
case "timestamp":
|
||||
return /** @satisfies {Height} */ (5);
|
||||
case "date":
|
||||
return /** @satisfies {DateIndex} */ (0);
|
||||
case "week":
|
||||
return /** @satisfies {WeekIndex} */ (22);
|
||||
case "month":
|
||||
return /** @satisfies {MonthIndex} */ (7);
|
||||
case "quarter":
|
||||
return /** @satisfies {QuarterIndex} */ (19);
|
||||
case "year":
|
||||
return /** @satisfies {YearIndex} */ (23);
|
||||
case "decade":
|
||||
return /** @satisfies {DecadeIndex} */ (1);
|
||||
default:
|
||||
throw Error("Unsupported");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const formatters = {
|
||||
@@ -1142,15 +1257,6 @@ function createUtils() {
|
||||
return 0;
|
||||
return this.differenceBetween(date, new Date("2009-01-09"));
|
||||
},
|
||||
/**
|
||||
* @param {Time} time
|
||||
*/
|
||||
fromTime(time) {
|
||||
return typeof time === "string"
|
||||
? new Date(time)
|
||||
: // @ts-ignore
|
||||
new Date(time.year, time.month, time.day);
|
||||
},
|
||||
/**
|
||||
* @param {Date} start
|
||||
*/
|
||||
@@ -1333,62 +1439,6 @@ function createUtils() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Index} index
|
||||
*/
|
||||
function vecIndexToString(index) {
|
||||
switch (index) {
|
||||
case /** @satisfies {DateIndex} */ (0):
|
||||
return "dateindex";
|
||||
case /** @satisfies {DecadeIndex} */ (1):
|
||||
return "decadeindex";
|
||||
case /** @satisfies {DifficultyEpoch} */ (2):
|
||||
return "difficultyepoch";
|
||||
case /** @satisfies {EmptyOutputIndex} */ (3):
|
||||
return "emptyoutputindex";
|
||||
case /** @satisfies {HalvingEpoch} */ (4):
|
||||
return "halvingepoch";
|
||||
case /** @satisfies {Height} */ (5):
|
||||
return "height";
|
||||
case /** @satisfies {InputIndex} */ (6):
|
||||
return "inputindex";
|
||||
case /** @satisfies {MonthIndex} */ (7):
|
||||
return "monthindex";
|
||||
case /** @satisfies {OpReturnIndex} */ (8):
|
||||
return "opreturnindex";
|
||||
case /** @satisfies {OutputIndex} */ (9):
|
||||
return "outputindex";
|
||||
case /** @satisfies {P2AIndex} */ (10):
|
||||
return "p2aindex";
|
||||
case /** @satisfies {P2MSIndex} */ (11):
|
||||
return "p2msindex";
|
||||
case /** @satisfies {P2PK33Index} */ (12):
|
||||
return "p2pk33index";
|
||||
case /** @satisfies {P2PK65Index} */ (13):
|
||||
return "p2pk65index";
|
||||
case /** @satisfies {P2PKHIndex} */ (14):
|
||||
return "p2pkhindex";
|
||||
case /** @satisfies {P2SHIndex} */ (15):
|
||||
return "p2shindex";
|
||||
case /** @satisfies {P2TRIndex} */ (16):
|
||||
return "p2trindex";
|
||||
case /** @satisfies {P2WPKHIndex} */ (17):
|
||||
return "p2wpkhindex";
|
||||
case /** @satisfies {P2WSHIndex} */ (18):
|
||||
return "p2wshindex";
|
||||
case /** @satisfies {QuarterIndex} */ (19):
|
||||
return "quarterindex";
|
||||
case /** @satisfies {TxIndex} */ (20):
|
||||
return "txindex";
|
||||
case /** @satisfies {UnknownOutputIndex} */ (21):
|
||||
return "unknownoutputindex";
|
||||
case /** @satisfies {WeekIndex} */ (22):
|
||||
return "weekindex";
|
||||
case /** @satisfies {YearIndex} */ (23):
|
||||
return "yearindex";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Index} index
|
||||
* @param {VecId} vecId
|
||||
@@ -1396,7 +1446,7 @@ function createUtils() {
|
||||
* @param {number} [to]
|
||||
*/
|
||||
function genPath(index, vecId, from, to) {
|
||||
let path = `/query?index=${vecIndexToString(index)}&values=${vecId}`;
|
||||
let path = `/query?index=${serde.index.serialize(index)}&values=${vecId}`;
|
||||
if (from !== undefined) {
|
||||
path += `&from=${from}`;
|
||||
}
|
||||
@@ -1828,9 +1878,8 @@ function initWebSockets(signals, utils) {
|
||||
|
||||
/**
|
||||
* @param {(candle: CandlestickData) => void} callback
|
||||
* @param {number} interval
|
||||
*/
|
||||
function krakenCandleWebSocketCreator(callback, interval) {
|
||||
function krakenCandleWebSocketCreator(callback) {
|
||||
const ws = new WebSocket("wss://ws.kraken.com/v2");
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
@@ -1853,14 +1902,10 @@ function initWebSockets(signals, utils) {
|
||||
|
||||
const { interval_begin, open, high, low, close } = result.data.at(-1);
|
||||
|
||||
const date = new Date(interval_begin);
|
||||
|
||||
const dateStr = utils.date.toString(date);
|
||||
|
||||
/** @type {CandlestickData} */
|
||||
const candle = {
|
||||
index: -1,
|
||||
time: dateStr,
|
||||
// index: -1,
|
||||
time: new Date(interval_begin).valueOf() / 1000,
|
||||
open: Number(open),
|
||||
high: Number(high),
|
||||
low: Number(low),
|
||||
@@ -1874,8 +1919,9 @@ function initWebSockets(signals, utils) {
|
||||
return ws;
|
||||
}
|
||||
|
||||
/** @type {ReturnType<typeof createWebsocket<CandlestickData>>} */
|
||||
const kraken1dCandle = createWebsocket((callback) =>
|
||||
krakenCandleWebSocketCreator(callback, 1440),
|
||||
krakenCandleWebSocketCreator(callback),
|
||||
);
|
||||
|
||||
kraken1dCandle.open();
|
||||
@@ -2079,21 +2125,18 @@ function main() {
|
||||
env,
|
||||
signals,
|
||||
utils,
|
||||
webSockets,
|
||||
qrcode,
|
||||
});
|
||||
|
||||
// const urlSelected = utils.url.pathnameToSelectedId();
|
||||
// function createWindowPopStateEvent() {
|
||||
// window.addEventListener("popstate", (event) => {
|
||||
// const urlSelected = utils.url.pathnameToSelectedId();
|
||||
// const option = options.list.find((option) => urlSelected === option.id);
|
||||
// if (option) {
|
||||
// options.selected.set(option);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// createWindowPopStateEvent();
|
||||
// window.addEventListener("popstate", (_) => {
|
||||
// const urlSelected = utils.url.pathnameToSelectedId();
|
||||
// const option = options.list.find(
|
||||
// (option) => urlSelected === option.id,
|
||||
// );
|
||||
// if (option) {
|
||||
// options.selected.set(option);
|
||||
// }
|
||||
// });
|
||||
|
||||
function initSelected() {
|
||||
let firstRun = true;
|
||||
@@ -2120,6 +2163,7 @@ function main() {
|
||||
let firstTimeLoadingSimulation = true;
|
||||
|
||||
signals.createEffect(options.selected, (option) => {
|
||||
console.log(utils.url.pathnameToSelectedId(), option.id);
|
||||
if (previousElement) {
|
||||
previousElement.hidden = true;
|
||||
utils.url.resetParams(option);
|
||||
@@ -2139,25 +2183,22 @@ function main() {
|
||||
|
||||
if (firstTimeLoadingChart) {
|
||||
const lightweightCharts = packages.lightweightCharts();
|
||||
const chartScript = import("./chart.js");
|
||||
utils.dom.importStyleAndThen("/styles/chart.css", () =>
|
||||
chartScript.then(({ init: initChartsElement }) =>
|
||||
lightweightCharts.then((lightweightCharts) =>
|
||||
signals.runWithOwner(owner, () =>
|
||||
initChartsElement({
|
||||
colors,
|
||||
elements,
|
||||
lightweightCharts,
|
||||
selected: /** @type {Accessor<ChartOption>} */ (
|
||||
chartOption
|
||||
),
|
||||
signals,
|
||||
utils,
|
||||
webSockets,
|
||||
vecsResources,
|
||||
vecIdToIndexes,
|
||||
}),
|
||||
),
|
||||
import("./chart.js").then(({ init: initChartsElement }) =>
|
||||
lightweightCharts.then((lightweightCharts) =>
|
||||
signals.runWithOwner(owner, () =>
|
||||
initChartsElement({
|
||||
colors,
|
||||
elements,
|
||||
lightweightCharts,
|
||||
option: /** @type {Accessor<ChartOption>} */ (
|
||||
chartOption
|
||||
),
|
||||
signals,
|
||||
utils,
|
||||
webSockets,
|
||||
vecsResources,
|
||||
vecIdToIndexes,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -2170,20 +2211,17 @@ function main() {
|
||||
element = elements.table;
|
||||
|
||||
if (firstTimeLoadingTable) {
|
||||
const tableScript = import("./table.js");
|
||||
utils.dom.importStyleAndThen("/styles/table.css", () =>
|
||||
tableScript.then(({ init }) =>
|
||||
signals.runWithOwner(owner, () =>
|
||||
init({
|
||||
colors,
|
||||
elements,
|
||||
signals,
|
||||
utils,
|
||||
vecsResources,
|
||||
option,
|
||||
vecIdToIndexes,
|
||||
}),
|
||||
),
|
||||
import("./table.js").then(({ init }) =>
|
||||
signals.runWithOwner(owner, () =>
|
||||
init({
|
||||
colors,
|
||||
elements,
|
||||
signals,
|
||||
utils,
|
||||
vecsResources,
|
||||
option,
|
||||
vecIdToIndexes,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2198,24 +2236,19 @@ function main() {
|
||||
|
||||
if (firstTimeLoadingSimulation) {
|
||||
const lightweightCharts = packages.lightweightCharts();
|
||||
const simulationScript = import("./simulation.js");
|
||||
utils.dom.importStyleAndThen(
|
||||
"/styles/simulation.css",
|
||||
() =>
|
||||
simulationScript.then(({ init }) =>
|
||||
lightweightCharts.then((lightweightCharts) =>
|
||||
signals.runWithOwner(owner, () =>
|
||||
init({
|
||||
colors,
|
||||
elements,
|
||||
lightweightCharts,
|
||||
signals,
|
||||
utils,
|
||||
vecsResources,
|
||||
}),
|
||||
),
|
||||
),
|
||||
import("./simulation.js").then(({ init }) =>
|
||||
lightweightCharts.then((lightweightCharts) =>
|
||||
signals.runWithOwner(owner, () =>
|
||||
init({
|
||||
colors,
|
||||
elements,
|
||||
lightweightCharts,
|
||||
signals,
|
||||
utils,
|
||||
vecsResources,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
firstTimeLoadingSimulation = false;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @typedef {Height | DateIndex | WeekIndex | DifficultyEpoch | MonthIndex | QuarterIndex | YearIndex | HalvingEpoch | DecadeIndex} ChartableIndex
|
||||
* @typedef {Height | DateIndex | WeekIndex | MonthIndex | QuarterIndex | YearIndex | DecadeIndex} ChartableIndex
|
||||
*/
|
||||
/**
|
||||
* @template {readonly unknown[]} T
|
||||
@@ -16,31 +16,25 @@
|
||||
* @property {string} title
|
||||
* @property {boolean} [defaultActive]
|
||||
*
|
||||
* @typedef {Object} CreatePriceLine
|
||||
* @property {number} value
|
||||
*
|
||||
* @typedef {Object} CreatePriceLineOptions
|
||||
* @property {CreatePriceLine} createPriceLine
|
||||
*
|
||||
* @typedef {Object} BaselineSeriesBlueprintSpecific
|
||||
* @property {"Baseline"} type
|
||||
* @property {Color} [color]
|
||||
* @property {[Color, Color]} [colors]
|
||||
* @property {DeepPartial<BaselineStyleOptions & SeriesOptionsCommon & CreatePriceLineOptions>} [options]
|
||||
* @property {PartialBaselineStyleOptions} [options]
|
||||
* @property {Accessor<BaselineData[]>} [data]
|
||||
* @typedef {BaseSeriesBlueprint & BaselineSeriesBlueprintSpecific} BaselineSeriesBlueprint
|
||||
*
|
||||
* @typedef {Object} CandlestickSeriesBlueprintSpecific
|
||||
* @property {"Candlestick"} type
|
||||
* @property {Color} [color]
|
||||
* @property {DeepPartial<CandlestickStyleOptions & SeriesOptionsCommon>} [options]
|
||||
* @property {PartialCandlestickStyleOptions} [options]
|
||||
* @property {Accessor<CandlestickData[]>} [data]
|
||||
* @typedef {BaseSeriesBlueprint & CandlestickSeriesBlueprintSpecific} CandlestickSeriesBlueprint
|
||||
*
|
||||
* @typedef {Object} LineSeriesBlueprintSpecific
|
||||
* @property {"Line"} [type]
|
||||
* @property {Color} [color]
|
||||
* @property {DeepPartial<LineStyleOptions & SeriesOptionsCommon & CreatePriceLineOptions>} [options]
|
||||
* @property {PartialLineStyleOptions} [options]
|
||||
* @property {Accessor<LineData[]>} [data]
|
||||
* @typedef {BaseSeriesBlueprint & LineSeriesBlueprintSpecific} LineSeriesBlueprint
|
||||
*
|
||||
@@ -823,7 +817,7 @@ function createPartialOptions(colors) {
|
||||
* @param {string} args.name
|
||||
* @param {Color} [args.color]
|
||||
* @param {boolean} [args.defaultActive]
|
||||
* @param {DeepPartial<LineStyleOptions & SeriesOptionsCommon>} [args.options]
|
||||
* @param {PartialLineStyleOptions} [args.options]
|
||||
*/
|
||||
function createBaseSeries({ key, name, color, defaultActive, options }) {
|
||||
return /** @satisfies {AnyFetchedSeriesBlueprint} */ ({
|
||||
@@ -1512,7 +1506,9 @@ function createPartialOptions(colors) {
|
||||
}),
|
||||
/** @satisfies {FetchedBaselineSeriesBlueprint} */ ({
|
||||
type: "Baseline",
|
||||
key: `${fixKey(key)}net-realized-profit-and-loss-relative-to-realized-cap`,
|
||||
key: `${fixKey(
|
||||
key,
|
||||
)}net-realized-profit-and-loss-relative-to-realized-cap`,
|
||||
title: useGroupName ? name : "Net",
|
||||
color: useGroupName ? color : undefined,
|
||||
options: {
|
||||
@@ -1581,7 +1577,9 @@ function createPartialOptions(colors) {
|
||||
bottom: list.flatMap(({ color, name, key }) => [
|
||||
/** @satisfies {FetchedBaselineSeriesBlueprint} */ ({
|
||||
type: "Baseline",
|
||||
key: `${fixKey(key)}adjusted-spent-output-profit-ratio`,
|
||||
key: `${fixKey(
|
||||
key,
|
||||
)}adjusted-spent-output-profit-ratio`,
|
||||
title: useGroupName ? name : "asopr",
|
||||
color: useGroupName ? color : undefined,
|
||||
options: {
|
||||
@@ -1691,7 +1689,9 @@ function createPartialOptions(colors) {
|
||||
}),
|
||||
/** @satisfies {FetchedBaselineSeriesBlueprint} */ ({
|
||||
type: "Baseline",
|
||||
key: `${fixKey(key)}net-unrealized-profit-and-loss-relative-to-market-cap`,
|
||||
key: `${fixKey(
|
||||
key,
|
||||
)}net-unrealized-profit-and-loss-relative-to-market-cap`,
|
||||
title: useGroupName ? name : "Net",
|
||||
color: useGroupName ? color : undefined,
|
||||
options: {
|
||||
@@ -1987,7 +1987,7 @@ function createPartialOptions(colors) {
|
||||
top: [
|
||||
createBaseSeries({
|
||||
key: `${key}-dca-avg-price`,
|
||||
name: `dca avg. price`,
|
||||
name: `dca`,
|
||||
color: colors.orange,
|
||||
}),
|
||||
createBaseSeries({
|
||||
@@ -2841,7 +2841,7 @@ function createPartialOptions(colors) {
|
||||
},
|
||||
{
|
||||
name: "Status",
|
||||
url: () => "https://status.kibo.money/",
|
||||
url: () => "https://status.bitcoinresearchkit.org/",
|
||||
},
|
||||
{
|
||||
name: "Crates",
|
||||
@@ -2883,17 +2883,9 @@ function createPartialOptions(colors) {
|
||||
* @param {Signals} args.signals
|
||||
* @param {Env} args.env
|
||||
* @param {Utilities} args.utils
|
||||
* @param {WebSockets} args.webSockets
|
||||
* @param {Signal<string | null>} args.qrcode
|
||||
*/
|
||||
export function initOptions({
|
||||
colors,
|
||||
signals,
|
||||
env,
|
||||
utils,
|
||||
webSockets,
|
||||
qrcode,
|
||||
}) {
|
||||
export function initOptions({ colors, signals, env, utils, qrcode }) {
|
||||
const LS_SELECTED_KEY = `selected-id`;
|
||||
|
||||
const urlSelected = utils.url.pathnameToSelectedId();
|
||||
@@ -2937,9 +2929,8 @@ export function initOptions({
|
||||
* @param {Signal<string | null>} args.qrcode
|
||||
* @param {string} [args.name]
|
||||
* @param {string} [args.id]
|
||||
* @param {Owner | null} [args.owner]
|
||||
*/
|
||||
function createOptionElement({ option, frame, name, id, owner, qrcode }) {
|
||||
function createOptionElement({ option, frame, name, id, qrcode }) {
|
||||
if (option.kind === "url") {
|
||||
const href = option.url();
|
||||
|
||||
@@ -2989,13 +2980,7 @@ export function initOptions({
|
||||
});
|
||||
}
|
||||
|
||||
if (owner !== undefined) {
|
||||
signals.runWithOwner(owner, () => {
|
||||
createCheckEffect();
|
||||
});
|
||||
} else {
|
||||
createCheckEffect();
|
||||
}
|
||||
createCheckEffect();
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function init({
|
||||
* @param {number} args.min
|
||||
* @param {number} args.step
|
||||
* @param {number} [args.max]
|
||||
* @param {{createEffect: typeof CreateEffect}} args.signals
|
||||
* @param {Signals} args.signals
|
||||
*/
|
||||
createInputNumberElement({
|
||||
id,
|
||||
@@ -95,7 +95,7 @@ export function init({
|
||||
* @param {string} args.id
|
||||
* @param {string} args.title
|
||||
* @param {Signal<number | null>} args.signal
|
||||
* @param {{createEffect: typeof CreateEffect}} args.signals
|
||||
* @param {Signals} args.signals
|
||||
*/
|
||||
createInputDollar({ id, title, signal, signals }) {
|
||||
return this.createInputNumberElement({
|
||||
@@ -113,7 +113,7 @@ export function init({
|
||||
* @param {string} args.id
|
||||
* @param {string} args.title
|
||||
* @param {Signal<Date | null>} args.signal
|
||||
* @param {{createEffect: typeof CreateEffect}} args.signals
|
||||
* @param {Signals} args.signals
|
||||
*/
|
||||
createInputDate({ id, title, signal, signals }) {
|
||||
const input = window.document.createElement("input");
|
||||
@@ -410,7 +410,9 @@ export function init({
|
||||
* @param {string} param0.text
|
||||
*/
|
||||
function createColoredSpan({ color, text }) {
|
||||
return `<span style="color: ${colors[color]()}; font-weight: 500; text-transform: uppercase;
|
||||
return `<span style="color: ${colors[
|
||||
color
|
||||
]()}; font-weight: 500; text-transform: uppercase;
|
||||
font-size: var(--font-size-sm);">${text}</span>`;
|
||||
}
|
||||
|
||||
@@ -606,91 +608,70 @@ export function init({
|
||||
const owner = signals.getOwner();
|
||||
|
||||
const totalInvestedAmountData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
/** @type {LineData[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const bitcoinValueData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const bitcoinData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const resultData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const dollarsLeftData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const totalValueData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const investmentData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
/** @type {LineData[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const bitcoinData = signals.createSignal(/** @type {LineData[]} */ ([]), {
|
||||
equals: false,
|
||||
});
|
||||
const resultData = signals.createSignal(/** @type {LineData[]} */ ([]), {
|
||||
equals: false,
|
||||
});
|
||||
const dollarsLeftData = signals.createSignal(/** @type {LineData[]} */ ([]), {
|
||||
equals: false,
|
||||
});
|
||||
const totalValueData = signals.createSignal(/** @type {LineData[]} */ ([]), {
|
||||
equals: false,
|
||||
});
|
||||
const investmentData = signals.createSignal(/** @type {LineData[]} */ ([]), {
|
||||
equals: false,
|
||||
});
|
||||
const bitcoinAddedData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
/** @type {LineData[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const averagePricePaidData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
/** @type {LineData[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const bitcoinPriceData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const buyCountData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
/** @type {LineData[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const buyCountData = signals.createSignal(/** @type {LineData[]} */ ([]), {
|
||||
equals: false,
|
||||
});
|
||||
const totalFeesPaidData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const daysCountData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
/** @type {LineData[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const daysCountData = signals.createSignal(/** @type {LineData[]} */ ([]), {
|
||||
equals: false,
|
||||
});
|
||||
const profitableDaysRatioData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
/** @type {LineData[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const unprofitableDaysRatioData = signals.createSignal(
|
||||
/** @type {LineData<Time>[]} */ ([]),
|
||||
/** @type {LineData[]} */ ([]),
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
@@ -700,7 +681,6 @@ export function init({
|
||||
|
||||
lightweightCharts.createChartElement({
|
||||
index,
|
||||
owner,
|
||||
parent: resultsElement,
|
||||
signals,
|
||||
colors,
|
||||
@@ -746,7 +726,6 @@ export function init({
|
||||
|
||||
lightweightCharts.createChartElement({
|
||||
index,
|
||||
owner,
|
||||
parent: resultsElement,
|
||||
signals,
|
||||
colors,
|
||||
@@ -772,7 +751,6 @@ export function init({
|
||||
|
||||
lightweightCharts.createChartElement({
|
||||
index,
|
||||
owner,
|
||||
parent: resultsElement,
|
||||
signals,
|
||||
colors,
|
||||
@@ -804,7 +782,6 @@ export function init({
|
||||
|
||||
lightweightCharts.createChartElement({
|
||||
index,
|
||||
owner,
|
||||
parent: resultsElement,
|
||||
signals,
|
||||
colors,
|
||||
@@ -837,7 +814,6 @@ export function init({
|
||||
vecsResources,
|
||||
utils,
|
||||
elements,
|
||||
owner,
|
||||
config: [
|
||||
{
|
||||
unit: "percentage",
|
||||
@@ -933,8 +909,7 @@ export function init({
|
||||
let lastSatsAdded = 0;
|
||||
|
||||
range.forEach((date, index) => {
|
||||
const year = date.getUTCFullYear();
|
||||
const time = utils.date.toString(date);
|
||||
const time = date.valueOf() / 1000;
|
||||
|
||||
if (topUpFrequency.isTriggerDay(date)) {
|
||||
dollars += topUpAmount;
|
||||
@@ -1099,7 +1074,12 @@ export function init({
|
||||
);
|
||||
const serDailyInvestment = c("emerald", fd(dailyInvestment));
|
||||
const setLastSatsAdded = c("orange", f(lastSatsAdded));
|
||||
p2.innerHTML = `You would've last bought ${c("blue", dayDiff ? `${f(dayDiff)} ${dayDiff > 1 ? "days" : "day"} ago` : "today")} and exchanged ${serDailyInvestment} for approximately ${setLastSatsAdded} Satoshis`;
|
||||
p2.innerHTML = `You would've last bought ${c(
|
||||
"blue",
|
||||
dayDiff
|
||||
? `${f(dayDiff)} ${dayDiff > 1 ? "days" : "day"} ago`
|
||||
: "today",
|
||||
)} and exchanged ${serDailyInvestment} for approximately ${setLastSatsAdded} Satoshis`;
|
||||
|
||||
const serProfitableDaysRatio = c("green", fp(profitableDaysRatio));
|
||||
const serUnprofitableDaysRatio = c(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// File auto-generated, any modifications will be overwritten
|
||||
//
|
||||
|
||||
export const VERSION = "v0.0.48";
|
||||
export const VERSION = "v0.0.58";
|
||||
|
||||
/** @typedef {0} DateIndex */
|
||||
/** @typedef {1} DecadeIndex */
|
||||
@@ -1251,6 +1251,7 @@ export function createVecIdToIndexes() {
|
||||
"fee-75p": [5],
|
||||
"fee-90p": [5],
|
||||
"fee-average": [0, 1, 2, 5, 7, 19, 22, 23],
|
||||
"fee-in-btc": [20],
|
||||
"fee-in-btc-10p": [5],
|
||||
"fee-in-btc-25p": [5],
|
||||
"fee-in-btc-75p": [5],
|
||||
@@ -1260,6 +1261,7 @@ export function createVecIdToIndexes() {
|
||||
"fee-in-btc-median": [5],
|
||||
"fee-in-btc-min": [0, 1, 2, 5, 7, 19, 22, 23],
|
||||
"fee-in-btc-sum": [0, 1, 2, 5, 7, 19, 22, 23],
|
||||
"fee-in-usd": [20],
|
||||
"fee-in-usd-10p": [5],
|
||||
"fee-in-usd-25p": [5],
|
||||
"fee-in-usd-75p": [5],
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
const CACHE_NAME = "cache";
|
||||
// DO NOT CHANGE, Exact format is expected in `brk_bundler`
|
||||
const CACHE_VERSION = "__VERSION__";
|
||||
|
||||
const SHELL_FILES = ["/", "/index.html"];
|
||||
|
||||
/** @type {ServiceWorkerGlobalScope} */
|
||||
const sw = /** @type {any} */ (self);
|
||||
|
||||
sw.addEventListener("install", (event) => {
|
||||
console.log("sw: install");
|
||||
event.waitUntil(sw.skipWaiting());
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(CACHE_VERSION)
|
||||
.then((c) => c.addAll(SHELL_FILES))
|
||||
.then(() => sw.skipWaiting()),
|
||||
);
|
||||
});
|
||||
|
||||
sw.addEventListener("activate", (event) => {
|
||||
console.log("sw: active");
|
||||
sw.clients.claim();
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then((keys) =>
|
||||
Promise.all(
|
||||
keys.filter((key) => key !== "api").map((key) => caches.delete(key)),
|
||||
Promise.all([
|
||||
sw.clients.claim(),
|
||||
caches
|
||||
.keys()
|
||||
.then((keys) =>
|
||||
Promise.all(
|
||||
keys
|
||||
.filter((key) => key !== "api" && key !== CACHE_VERSION)
|
||||
.map((key) => caches.delete(key)),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -42,7 +54,7 @@ sw.addEventListener("fetch", (event) => {
|
||||
return; // let the browser handle it
|
||||
}
|
||||
|
||||
const cache = caches.open(CACHE_NAME);
|
||||
const cache = caches.open(CACHE_VERSION);
|
||||
|
||||
// 2) NAVIGATION: network‐first on your shell
|
||||
if (req.mode === "navigate") {
|
||||
@@ -76,14 +88,13 @@ sw.addEventListener("fetch", (event) => {
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(async () => {
|
||||
return caches
|
||||
.catch(async () =>
|
||||
caches
|
||||
.match(req)
|
||||
.then((cached) => {
|
||||
return cached || indexHTMLOrOffline();
|
||||
})
|
||||
.catch(indexHTMLOrOffline);
|
||||
})
|
||||
.catch(indexHTMLOrOffline),
|
||||
.catch(indexHTMLOrOffline),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
#charts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
padding: var(--main-padding);
|
||||
|
||||
header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: -2rem;
|
||||
padding-left: var(--main-padding);
|
||||
margin-left: var(--negative-main-padding);
|
||||
padding-right: var(--main-padding);
|
||||
margin-right: var(--negative-main-padding);
|
||||
|
||||
& > * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
> .chart > legend,
|
||||
> fieldset {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.lightweight-chart {
|
||||
z-index: 40;
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
#simulation {
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
padding: var(--main-padding);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
overflow-y: auto;
|
||||
|
||||
> div:first-child {
|
||||
border-bottom: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex-direction: row;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: var(--bottom-area);
|
||||
}
|
||||
|
||||
> div:first-child {
|
||||
max-width: var(--default-main-width);
|
||||
border-right: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
overflow-x: hidden;
|
||||
|
||||
p {
|
||||
text-wrap: pretty;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
> span {
|
||||
display: block;
|
||||
}
|
||||
small {
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
flex: none;
|
||||
height: 400px;
|
||||
|
||||
.lightweight-chart {
|
||||
margin-left: calc(var(--negative-main-padding) * 0.75);
|
||||
|
||||
fieldset {
|
||||
margin-left: -0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
#table {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
padding: var(--main-padding);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
font-size: var(--font-size-xs);
|
||||
line--line-height: var(--line-height-xs);
|
||||
font-weight: 450;
|
||||
margin-left: var(--negative-main-padding);
|
||||
margin-right: var(--negative-main-padding);
|
||||
|
||||
table {
|
||||
z-index: 10;
|
||||
border-top-width: 1px;
|
||||
border-style: dashed !important;
|
||||
/* width: 100%; */
|
||||
line-height: var(--line-height-sm);
|
||||
text-transform: uppercase;
|
||||
table-layout: auto;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
/* border: 3px solid purple; */
|
||||
/* min-height: 100%; */
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
/* border-top: 1px; */
|
||||
border-right: 1px;
|
||||
border-bottom: 1px;
|
||||
border-color: var(--off-color);
|
||||
border-style: dashed !important;
|
||||
padding: 0.25rem 0.75rem;
|
||||
}
|
||||
|
||||
td {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
a {
|
||||
margin: -0.2rem 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
th:first-child {
|
||||
padding-left: var(--main-padding);
|
||||
}
|
||||
|
||||
th[scope="col"] {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--background-color);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 0.275rem;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
text-transform: lowercase;
|
||||
color: var(--off-color);
|
||||
text-align: left;
|
||||
|
||||
&:first-child {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
> span {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> button {
|
||||
padding: 0 0.25rem;
|
||||
margin: 0 -0.25rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
button:nth-of-type(1) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
button:nth-of-type(2) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
margin-right: -4px;
|
||||
/* width: 100%; */
|
||||
}
|
||||
|
||||
tbody {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
> button {
|
||||
padding: 1rem;
|
||||
min-width: 10rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
border-top-width: 1px;
|
||||
width: 100%;
|
||||
border-bottom-width: 1px;
|
||||
border-style: dashed !important;
|
||||
|
||||
> span {
|
||||
text-align: left;
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
|
||||
Reference in New Issue
Block a user