Compare commits

...

22 Commits

Author SHA1 Message Date
nym21 098f6de047 release: v0.0.61 2025-06-15 17:30:49 +02:00
nym21 1b0f90fd68 release: v0.0.60 2025-06-15 17:27:41 +02:00
nym21 12252f407b computer: fix open of ohlc if fetched from different API than prev ohlc 2025-06-15 17:27:16 +02:00
nym21 3b6e3f47ab release: v0.0.59 2025-06-15 12:40:46 +02:00
nym21 6a9ac9b025 brk: fix bundler use + bundler: remove minify html crate 2025-06-15 12:39:50 +02:00
nym21 ae6aa4088b release: v0.0.58 2025-06-15 01:50:22 +02:00
nym21 c08f431180 bundler: deploy brk_rolldown + fix edge case 2025-06-15 01:50:01 +02:00
nym21 123c1f56e9 release: v0.0.57 2025-06-14 22:47:57 +02:00
nym21 35ac65a864 server: update cache control for bundled websites 2025-06-14 22:47:26 +02:00
nym21 e9f362cc87 bundler: init working version 2025-06-14 20:17:49 +02:00
nym21 65685c23e1 release: v0.0.56 2025-06-13 18:03:28 +02:00
nym21 2f74748cea computer: stateful: reset when reorg detected 2025-06-13 18:03:09 +02:00
nym21 f477bd66f3 release: v0.0.55 2025-06-13 10:23:38 +02:00
nym21 d7d77ae8f0 global: multiple fixes 2025-06-13 10:22:03 +02:00
nym21 31110a740d release: v0.0.54 2025-06-12 22:18:36 +02:00
nym21 b64d8b1d7f release: v0.0.53 2025-06-12 22:16:33 +02:00
nym21 c46006aacc web: filter possible index choices in charts 2025-06-12 22:09:33 +02:00
nym21 92f81b1493 web: fix css 2025-06-12 20:23:23 +02:00
nym21 70213cfc8f websites: default: add auto price series type 2025-06-12 18:41:56 +02:00
nym21 8a82bf5c50 websites: default: add live price 2025-06-12 18:10:24 +02:00
nym21 37405384a2 vec: fixed compressed, still slow par read, cli: made raw the default 2025-06-12 16:31:54 +02:00
nym21 54ea6cc53b indexer: only raw format + global: fixes 2025-06-12 12:33:43 +02:00
87 changed files with 5203 additions and 2619 deletions
+1
View File
@@ -3,6 +3,7 @@
# Builds
target
dist
# Copies
*\ copy*
Generated
+1249 -121
View File
File diff suppressed because it is too large Load Diff
+16 -15
View File
@@ -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"
+1
View File
@@ -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
+3
View File
@@ -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 }
+1
View File
@@ -0,0 +1 @@
fn main() {}
+7
View File
@@ -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;
+15
View File
@@ -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 }
+142
View File
@@ -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(())
}
+1 -1
View File
@@ -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
View File
@@ -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,
+1 -1
View File
@@ -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()?;
+13 -3
View File
@@ -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()
+1
View File
@@ -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...");
+15 -13
View File
@@ -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(());
}
+5 -1
View File
@@ -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)]
+2
View File
@@ -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::*;
+12
View File
@@ -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()),
}
}
+14 -1
View File
@@ -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(),
+2 -2
View File
@@ -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(
+5 -5
View File
@@ -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();
}
}
+1 -2
View File
@@ -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()?;
+1 -7
View File
@@ -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);
+2 -9
View File
@@ -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(())
}
+34 -38
View File
@@ -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,
)?,
})
}
+49 -42
View File
@@ -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();
+1 -1
View File
@@ -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 }
+1 -1
View File
@@ -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);
+3 -2
View File
@@ -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]
+2 -2
View File
@@ -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 {
+1
View File
@@ -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((
(
+26 -46
View File
@@ -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);
-41
View File
@@ -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
}
-1
View File
@@ -3,7 +3,6 @@ use axum::{Router, routing::get};
use super::AppState;
mod file;
mod minify;
mod website;
use file::{file_handler, index_handler};
+15 -1
View File
@@ -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)
+11 -47
View File
@@ -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(),
+1 -1
View File
@@ -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,
}
+2 -1
View File
@@ -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]
+13 -5
View File
@@ -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(),
+5 -1
View File
@@ -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]
+7
View File
@@ -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] {
-33
View File
@@ -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
View File
@@ -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>
<!-- --- -->
+2 -2
View File
@@ -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",
-46
View File
@@ -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
-4
View File
@@ -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
+139 -131
View File
@@ -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;
+201 -71
View File
@@ -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 };
+3
View File
@@ -0,0 +1,3 @@
// DO NOT CHANGE, Exact format is expected in `brk_bundler`
// @ts-ignore
import("./main.js");
+214 -181
View File
@@ -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;
+19 -34
View File
@@ -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;
}
+42 -62
View File
@@ -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(
+3 -1
View File
@@ -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],
+26 -15
View File
@@ -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: networkfirst 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),
),
);
});
-39
View File
@@ -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;
}
}
-76
View File
@@ -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;
}
}
}
}
-144
View File
@@ -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
View File
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"strict": true,
"target": "ESNext",