Compare commits

...

10 Commits

Author SHA1 Message Date
nym21 5f1a3a9c8f release: v0.0.13 2025-04-04 18:31:31 +02:00
nym21 0767b3156d global: snapshot 2025-04-04 18:31:10 +02:00
nym21 9f16379b41 readme: add warning 2025-04-04 18:23:03 +02:00
nym21 be632aaf37 kibo: fix simulation 2025-04-04 17:44:22 +02:00
nym21 118c87faf7 kibo: snapshot 2025-04-04 11:30:59 +02:00
nym21 ec1e53d566 kibo: finished converting ts types to jsdoc 2025-04-04 10:54:44 +02:00
nym21 6a17ee414a kibo: move types around 2025-04-04 00:40:40 +02:00
nym21 6700686e4b website: signals: upgrade to tresshaked v0.2.4 2025-04-03 15:15:55 +02:00
nym21 e8c34dd59b global: snapshot 2025-04-03 14:31:39 +02:00
nym21 4c2da31bb3 server: version repo url when not in dev mode 2025-04-02 16:49:35 +02:00
76 changed files with 3211 additions and 11642 deletions
-1
View File
@@ -29,7 +29,6 @@ docker/kibo
# Types
paths.d.ts
vecid-to-indexes.d.ts
# Outputs
_outputs
Generated
+138 -26
View File
@@ -368,7 +368,7 @@ dependencies = [
[[package]]
name = "brk"
version = "0.0.12"
version = "0.0.13"
dependencies = [
"brk_cli",
"brk_computer",
@@ -385,7 +385,7 @@ dependencies = [
[[package]]
name = "brk_cli"
version = "0.0.12"
version = "0.0.13"
dependencies = [
"brk_computer",
"brk_core",
@@ -406,7 +406,7 @@ dependencies = [
[[package]]
name = "brk_computer"
version = "0.0.12"
version = "0.0.13"
dependencies = [
"brk_core",
"brk_exit",
@@ -421,7 +421,7 @@ dependencies = [
[[package]]
name = "brk_core"
version = "0.0.12"
version = "0.0.13"
dependencies = [
"bitcoin",
"bitcoincore-rpc",
@@ -438,7 +438,7 @@ dependencies = [
[[package]]
name = "brk_exit"
version = "0.0.12"
version = "0.0.13"
dependencies = [
"brk_logger",
"ctrlc",
@@ -447,7 +447,7 @@ dependencies = [
[[package]]
name = "brk_fetcher"
version = "0.0.12"
version = "0.0.13"
dependencies = [
"brk_core",
"brk_logger",
@@ -460,7 +460,7 @@ dependencies = [
[[package]]
name = "brk_indexer"
version = "0.0.12"
version = "0.0.13"
dependencies = [
"bitcoin",
"bitcoincore-rpc",
@@ -479,7 +479,7 @@ dependencies = [
[[package]]
name = "brk_logger"
version = "0.0.12"
version = "0.0.13"
dependencies = [
"color-eyre",
"env_logger",
@@ -489,7 +489,7 @@ dependencies = [
[[package]]
name = "brk_parser"
version = "0.0.12"
version = "0.0.13"
dependencies = [
"bitcoin",
"bitcoincore-rpc",
@@ -504,7 +504,7 @@ dependencies = [
[[package]]
name = "brk_query"
version = "0.0.12"
version = "0.0.13"
dependencies = [
"brk_computer",
"brk_indexer",
@@ -520,7 +520,7 @@ dependencies = [
[[package]]
name = "brk_server"
version = "0.0.12"
version = "0.0.13"
dependencies = [
"axum",
"brk_computer",
@@ -540,12 +540,14 @@ dependencies = [
"serde",
"tokio",
"tower-http",
"tracing",
"tracing-subscriber",
"zip",
]
[[package]]
name = "brk_vec"
version = "0.0.12"
version = "0.0.13"
dependencies = [
"memmap2",
"rayon",
@@ -1115,9 +1117,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
dependencies = [
"crc32fast",
"miniz_oxide 0.8.5",
@@ -1348,9 +1350,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
dependencies = [
"bytes",
"futures-util",
@@ -1364,9 +1366,9 @@ dependencies = [
[[package]]
name = "iana-time-zone"
version = "0.1.62"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -1502,10 +1504,11 @@ dependencies = [
[[package]]
name = "jobserver"
version = "0.1.32"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [
"getrandom 0.3.2",
"libc",
]
@@ -1710,6 +1713,16 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
@@ -1765,6 +1778,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owo-colors"
version = "3.5.0"
@@ -2474,9 +2493,9 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
version = "1.0.3"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96"
checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
dependencies = [
"bitflags",
"errno",
@@ -3020,6 +3039,7 @@ dependencies = [
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
@@ -3042,9 +3062,21 @@ checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "tracing-core"
version = "0.1.33"
@@ -3065,15 +3097,29 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"nu-ansi-term",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
]
[[package]]
@@ -3239,12 +3285,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "windows-core"
version = "0.52.0"
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"windows-targets",
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
@@ -3253,6 +3347,24 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-result"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
+1 -1
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.12"
package.version = "0.0.13"
package.repository = "https://github.com/bitcoinresearchkit/brk"
[profile.release]
+9 -1
View File
@@ -34,6 +34,14 @@
</a>
</p>
> **WARNING**
>
> This project is still a work in progress and while it's much better in many ways than its previous version ([kibo v0.5](https://github.com/kibo-money/kibo)), it doesn't yet include all of those datasets. If you're interested in having everything right now, please use the latter until feature parity is achieved.
>
> The explorer part (mempool.space/electrs) is also not viable just yet.
>
> Stay tuned and please be patient, it's a lot of work !
The Bitcoin Research Kit is a high-performance toolchain designed to parse, index, compute, serve and visualize data from a Bitcoin Core node, enabling users to gain deeper insights into the Bitcoin network.
In other words it's an alternative to [Glassnode](https://glassnode.com), [mempool.space](https://mempool.space/) and [electrs](https://github.com/romanz/electrs) all in one package with a particular focus on simplicity and the self-hosting experience.
@@ -82,7 +90,7 @@ Heartfelt thanks go out to every donor on [Nostr](https://primal.net/p/npub1jagm
If you'd like to have your own instance hosted for you please contact [hosting@bitcoinresearchkit.org](mailto:hosting@bitcoinresearchkit.org).
- 2 separate servers (1 GB/s each) with different ISPs and Cloudflare integration for enhanced performance and optimal availability
- 2 separate dedicated servers (1 GB/s each) with different ISPs and Cloudflare integration for enhanced performance and optimal availability
- Direct communication for feature requests and support
- Updates delivered at your convenience
- Optional subdomains: `*.bitcoinresearchkit.org`, `*.kibo.money` and `*.satonomics.xyz`
-1
View File
@@ -1,5 +1,4 @@
use brk_computer::Computer;
use brk_fetcher::Fetcher;
use brk_indexer::Indexer;
use brk_query::{Index, Output, Params as QueryParams, Query, Tabled, Value};
use tabled::settings::Style;
+4 -2
View File
@@ -12,8 +12,10 @@ pub struct Stores {
impl Stores {
pub fn import(path: &Path) -> color_eyre::Result<Self> {
let address_to_utxos_received = Store::import(&path.join("address_to_utxos_received"), Version::from(1))?;
let address_to_utxos_spent = Store::import(&path.join("address_to_utxos_spent"), Version::from(1))?;
let address_to_utxos_received =
Store::import(&path.join("address_to_utxos_received"), Version::ONE)?;
let address_to_utxos_spent =
Store::import(&path.join("address_to_utxos_spent"), Version::ONE)?;
Ok(Self {
address_to_utxos_received,
+7 -7
View File
@@ -137,7 +137,7 @@ where
F: FnMut((A, B, &mut Self, &mut brk_vec::StorableVec<A, B>)) -> (I, T),
{
self.validate_computed_version_or_reset_file(
Version::from(0) + self.version() + other.version(),
Version::ZERO + self.version() + other.version(),
)?;
let index = max_from.min(A::from(self.len()));
@@ -160,7 +160,7 @@ where
T: StoredIndex,
{
self.validate_computed_version_or_reset_file(
Version::from(0) + self.version() + other.version(),
Version::ZERO + self.version() + other.version(),
)?;
let index = max_from.min(self.vec.get_last()?.cloned().unwrap_or_default());
@@ -188,7 +188,7 @@ where
T: StoredIndex,
{
self.validate_computed_version_or_reset_file(
Version::from(0) + self.version() + first_indexes.version() + last_indexes.version(),
Version::ZERO + self.version() + first_indexes.version() + last_indexes.version(),
)?;
let index = max_from.min(T::from(self.len()));
@@ -213,7 +213,7 @@ where
T: Copy + From<usize> + CheckedSub<T> + StoredIndex,
{
self.validate_computed_version_or_reset_file(
Version::from(0) + self.version() + first_indexes.version(),
Version::ZERO + self.version() + first_indexes.version(),
)?;
let index = max_from.min(I::from(self.len()));
@@ -250,7 +250,7 @@ where
<T2 as TryInto<T>>::Error: error::Error + 'static,
{
self.validate_computed_version_or_reset_file(
Version::from(0) + self.version() + first_indexes.version() + last_indexes.version(),
Version::ZERO + self.version() + first_indexes.version() + last_indexes.version(),
)?;
let index = max_from.min(I::from(self.len()));
@@ -278,7 +278,7 @@ where
A: StoredIndex + StoredType,
{
self.validate_computed_version_or_reset_file(
Version::from(0) + self.version() + self_to_other.version() + other_to_self.version(),
Version::ZERO + self.version() + self_to_other.version() + other_to_self.version(),
)?;
let index = max_from.min(I::from(self.len()));
@@ -302,7 +302,7 @@ where
<T2 as TryInto<T>>::Error: error::Error + 'static,
{
self.validate_computed_version_or_reset_file(
Version::from(0) + self.version() + first_indexes.version() + last_indexes.version(),
Version::ZERO + self.version() + first_indexes.version() + last_indexes.version(),
)?;
let index = max_from.min(I::from(self.len()));
@@ -25,7 +25,7 @@ impl Vecs {
Ok(Self {
height_to_block_interval: StorableVec::forced_import(
&path.join("height_to_block_interval"),
Version::from(1),
Version::ONE,
compressed,
)?,
indexes_to_block_interval_stats: StorableVecsStatsFromHeight::forced_import(
@@ -38,12 +38,12 @@ impl Vecs {
)?,
dateindex_to_block_count: StorableVec::forced_import(
&path.join("dateindex_to_block_count"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_total_block_count: StorableVec::forced_import(
&path.join("dateindex_to_total_block_count"),
Version::from(1),
Version::ONE,
compressed,
)?,
})
+48 -48
View File
@@ -71,242 +71,242 @@ impl Vecs {
Ok(Self {
dateindex_to_date: StorableVec::forced_import(
&path.join("dateindex_to_date"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_dateindex: StorableVec::forced_import(
&path.join("dateindex_to_dateindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_first_height: StorableVec::forced_import(
&path.join("dateindex_to_first_height"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_last_height: StorableVec::forced_import(
&path.join("dateindex_to_last_height"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_real_date: StorableVec::forced_import(
&path.join("height_to_real_date"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_fixed_date: StorableVec::forced_import(
&path.join("height_to_fixed_date"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_dateindex: StorableVec::forced_import(
&path.join("height_to_dateindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_height: StorableVec::forced_import(
&path.join("height_to_height"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_last_txindex: StorableVec::forced_import(
&path.join("height_to_last_txindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
txindex_to_last_txinindex: StorableVec::forced_import(
&path.join("txindex_to_last_txinindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
txindex_to_last_txoutindex: StorableVec::forced_import(
&path.join("txindex_to_last_txoutindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
difficultyepoch_to_first_height: StorableVec::forced_import(
&path.join("difficultyepoch_to_first_height"),
Version::from(1),
Version::ONE,
compressed,
)?,
difficultyepoch_to_last_height: StorableVec::forced_import(
&path.join("difficultyepoch_to_last_height"),
Version::from(1),
Version::ONE,
compressed,
)?,
halvingepoch_to_first_height: StorableVec::forced_import(
&path.join("halvingepoch_to_first_height"),
Version::from(1),
Version::ONE,
compressed,
)?,
halvingepoch_to_last_height: StorableVec::forced_import(
&path.join("halvingepoch_to_last_height"),
Version::from(1),
Version::ONE,
compressed,
)?,
weekindex_to_first_dateindex: StorableVec::forced_import(
&path.join("weekindex_to_first_dateindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
weekindex_to_last_dateindex: StorableVec::forced_import(
&path.join("weekindex_to_last_dateindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
monthindex_to_first_dateindex: StorableVec::forced_import(
&path.join("monthindex_to_first_dateindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
monthindex_to_last_dateindex: StorableVec::forced_import(
&path.join("monthindex_to_last_dateindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
yearindex_to_first_monthindex: StorableVec::forced_import(
&path.join("yearindex_to_first_monthindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
yearindex_to_last_monthindex: StorableVec::forced_import(
&path.join("yearindex_to_last_monthindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
decadeindex_to_first_yearindex: StorableVec::forced_import(
&path.join("decadeindex_to_first_yearindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
decadeindex_to_last_yearindex: StorableVec::forced_import(
&path.join("decadeindex_to_last_yearindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_weekindex: StorableVec::forced_import(
&path.join("dateindex_to_weekindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_monthindex: StorableVec::forced_import(
&path.join("dateindex_to_monthindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
monthindex_to_yearindex: StorableVec::forced_import(
&path.join("monthindex_to_yearindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
yearindex_to_decadeindex: StorableVec::forced_import(
&path.join("yearindex_to_decadeindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_difficultyepoch: StorableVec::forced_import(
&path.join("height_to_difficultyepoch"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_halvingepoch: StorableVec::forced_import(
&path.join("height_to_halvingepoch"),
Version::from(1),
Version::ONE,
compressed,
)?,
weekindex_to_weekindex: StorableVec::forced_import(
&path.join("weekindex_to_weekindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
monthindex_to_monthindex: StorableVec::forced_import(
&path.join("monthindex_to_monthindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
yearindex_to_yearindex: StorableVec::forced_import(
&path.join("yearindex_to_yearindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
decadeindex_to_decadeindex: StorableVec::forced_import(
&path.join("decadeindex_to_decadeindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
difficultyepoch_to_difficultyepoch: StorableVec::forced_import(
&path.join("difficultyepoch_to_difficultyepoch"),
Version::from(1),
Version::ONE,
compressed,
)?,
halvingepoch_to_halvingepoch: StorableVec::forced_import(
&path.join("halvingepoch_to_halvingepoch"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_timestamp: StorableVec::forced_import(
&path.join("dateindex_to_timestamp"),
Version::from(1),
Version::ONE,
compressed,
)?,
decadeindex_to_timestamp: StorableVec::forced_import(
&path.join("decadeindex_to_timestamp"),
Version::from(1),
Version::ONE,
compressed,
)?,
difficultyepoch_to_timestamp: StorableVec::forced_import(
&path.join("difficultyepoch_to_timestamp"),
Version::from(1),
Version::ONE,
compressed,
)?,
halvingepoch_to_timestamp: StorableVec::forced_import(
&path.join("halvingepoch_to_timestamp"),
Version::from(1),
Version::ONE,
compressed,
)?,
monthindex_to_timestamp: StorableVec::forced_import(
&path.join("monthindex_to_timestamp"),
Version::from(1),
Version::ONE,
compressed,
)?,
weekindex_to_timestamp: StorableVec::forced_import(
&path.join("weekindex_to_timestamp"),
Version::from(1),
Version::ONE,
compressed,
)?,
yearindex_to_timestamp: StorableVec::forced_import(
&path.join("yearindex_to_timestamp"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_fixed_timestamp: StorableVec::forced_import(
&path.join("height_to_fixed_timestamp"),
Version::from(1),
Version::ONE,
compressed,
)?,
monthindex_to_quarterindex: StorableVec::forced_import(
&path.join("monthindex_to_quarterindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
quarterindex_to_first_monthindex: StorableVec::forced_import(
&path.join("quarterindex_to_first_monthindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
quarterindex_to_last_monthindex: StorableVec::forced_import(
&path.join("quarterindex_to_last_monthindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
quarterindex_to_quarterindex: StorableVec::forced_import(
&path.join("quarterindex_to_quarterindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
quarterindex_to_timestamp: StorableVec::forced_import(
&path.join("quarterindex_to_timestamp"),
Version::from(1),
Version::ONE,
compressed,
)?,
})
@@ -66,112 +66,112 @@ impl Vecs {
Ok(Self {
dateindex_to_ohlc_in_cents: StorableVec::forced_import(
&path.join("dateindex_to_ohlc_in_cents"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_ohlc: StorableVec::forced_import(
&path.join("dateindex_to_ohlc"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_close_in_cents: StorableVec::forced_import(
&path.join("dateindex_to_close_in_cents"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_close: StorableVec::forced_import(
&path.join("dateindex_to_close"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_high_in_cents: StorableVec::forced_import(
&path.join("dateindex_to_high_in_cents"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_high: StorableVec::forced_import(
&path.join("dateindex_to_high"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_low_in_cents: StorableVec::forced_import(
&path.join("dateindex_to_low_in_cents"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_low: StorableVec::forced_import(
&path.join("dateindex_to_low"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_open_in_cents: StorableVec::forced_import(
&path.join("dateindex_to_open_in_cents"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_open: StorableVec::forced_import(
&path.join("dateindex_to_open"),
Version::from(1),
Version::ONE,
compressed,
)?,
dateindex_to_sats_per_dollar: StorableVec::forced_import(
&path.join("dateindex_to_sats_per_dollar"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_ohlc_in_cents: StorableVec::forced_import(
&path.join("height_to_ohlc_in_cents"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_ohlc: StorableVec::forced_import(
&path.join("height_to_ohlc"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_close_in_cents: StorableVec::forced_import(
&path.join("height_to_close_in_cents"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_close: StorableVec::forced_import(
&path.join("height_to_close"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_high_in_cents: StorableVec::forced_import(
&path.join("height_to_high_in_cents"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_high: StorableVec::forced_import(
&path.join("height_to_high"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_low_in_cents: StorableVec::forced_import(
&path.join("height_to_low_in_cents"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_low: StorableVec::forced_import(
&path.join("height_to_low"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_open_in_cents: StorableVec::forced_import(
&path.join("height_to_open_in_cents"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_open: StorableVec::forced_import(
&path.join("height_to_open"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_sats_per_dollar: StorableVec::forced_import(
&path.join("height_to_sats_per_dollar"),
Version::from(1),
Version::ONE,
compressed,
)?,
timeindexes_to_open: StorableVecsStatsFromDate::forced_import(
@@ -226,33 +226,33 @@ impl Vecs {
)?,
weekindex_to_ohlc: StorableVec::forced_import(
&path.join("weekindex_to_ohlc"),
Version::from(1),
Version::ONE,
compressed,
)?,
difficultyepoch_to_ohlc: StorableVec::forced_import(
&path.join("difficultyepoch_to_ohlc"),
Version::from(1),
Version::ONE,
compressed,
)?,
monthindex_to_ohlc: StorableVec::forced_import(
&path.join("monthindex_to_ohlc"),
Version::from(1),
Version::ONE,
compressed,
)?,
quarterindex_to_ohlc: StorableVec::forced_import(
&path.join("quarterindex_to_ohlc"),
Version::from(1),
Version::ONE,
compressed,
)?,
yearindex_to_ohlc: StorableVec::forced_import(
&path.join("yearindex_to_ohlc"),
Version::from(1),
Version::ONE,
compressed,
)?,
// halvingepoch_to_ohlc: StorableVec::forced_import(&path.join("halvingepoch_to_ohlc"), Version::from(1), compressed)?,
// halvingepoch_to_ohlc: StorableVec::forced_import(&path.join("halvingepoch_to_ohlc"), Version::ONE, compressed)?,
decadeindex_to_ohlc: StorableVec::forced_import(
&path.join("decadeindex_to_ohlc"),
Version::from(1),
Version::ONE,
compressed,
)?,
})
@@ -39,48 +39,62 @@ where
let name = path.file_name().unwrap().to_str().unwrap().to_string();
let key = I::to_string().split("::").last().unwrap().to_lowercase();
let prefix = |s: &str| path.with_file_name(format!("{key}_to_{s}_{name}"));
let suffix = |s: &str| path.with_file_name(format!("{key}_to_{name}_{s}"));
let only_one_active = options.is_only_one_active();
let prefix = |s: &str| {
if only_one_active {
path.with_file_name(format!("{key}_to_{name}"))
} else {
path.with_file_name(format!("{key}_to_{s}_{name}"))
}
};
let suffix = |s: &str| {
if only_one_active {
path.with_file_name(format!("{key}_to_{name}"))
} else {
path.with_file_name(format!("{key}_to_{name}_{s}"))
}
};
let s = Self {
first: options.first.then(|| {
StorableVec::forced_import(&prefix("first"), Version::from(1), compressed).unwrap()
StorableVec::forced_import(&prefix("first"), Version::ONE, compressed).unwrap()
}),
last: options.last.then(|| {
StorableVec::forced_import(
&path.with_file_name(format!("{key}_to_{name}")),
Version::from(1),
Version::ONE,
compressed,
)
.unwrap()
}),
min: options.min.then(|| {
StorableVec::forced_import(&suffix("min"), Version::from(1), compressed).unwrap()
StorableVec::forced_import(&suffix("min"), Version::ONE, compressed).unwrap()
}),
max: options.max.then(|| {
StorableVec::forced_import(&suffix("max"), Version::from(1), compressed).unwrap()
StorableVec::forced_import(&suffix("max"), Version::ONE, compressed).unwrap()
}),
median: options.median.then(|| {
StorableVec::forced_import(&suffix("median"), Version::from(1), compressed).unwrap()
StorableVec::forced_import(&suffix("median"), Version::ONE, compressed).unwrap()
}),
average: options.average.then(|| {
StorableVec::forced_import(&suffix("average"), Version::from(1), compressed)
.unwrap()
StorableVec::forced_import(&suffix("average"), Version::ONE, compressed).unwrap()
}),
sum: options.sum.then(|| {
StorableVec::forced_import(&suffix("sum"), Version::from(1), compressed).unwrap()
StorableVec::forced_import(&suffix("sum"), Version::ONE, compressed).unwrap()
}),
_90p: options._90p.then(|| {
StorableVec::forced_import(&suffix("90p"), Version::from(1), compressed).unwrap()
StorableVec::forced_import(&suffix("90p"), Version::ONE, compressed).unwrap()
}),
_75p: options._75p.then(|| {
StorableVec::forced_import(&suffix("75p"), Version::from(1), compressed).unwrap()
StorableVec::forced_import(&suffix("75p"), Version::ONE, compressed).unwrap()
}),
_25p: options._25p.then(|| {
StorableVec::forced_import(&suffix("25p"), Version::from(1), compressed).unwrap()
StorableVec::forced_import(&suffix("25p"), Version::ONE, compressed).unwrap()
}),
_10p: options._10p.then(|| {
StorableVec::forced_import(&suffix("10p"), Version::from(1), compressed).unwrap()
StorableVec::forced_import(&suffix("10p"), Version::ONE, compressed).unwrap()
}),
};
@@ -564,4 +578,24 @@ impl StorableVecGeneatorOptions {
self._10p = false;
self
}
pub fn is_only_one_active(&self) -> bool {
[
self.average,
self.sum,
self.max,
self._90p,
self._75p,
self.median,
self._25p,
self._10p,
self.min,
self.first,
self.last,
]
.iter()
.filter(|b| **b)
.count()
== 1
}
}
@@ -33,52 +33,52 @@ impl Vecs {
fs::create_dir_all(path)?;
Ok(Self {
// height_to_fee: StorableVec::forced_import(&path.join("height_to_fee"), Version::from(1))?,
// height_to_fee: StorableVec::forced_import(&path.join("height_to_fee"), Version::ONE)?,
// height_to_input_count: StorableVec::forced_import(
// &path.join("height_to_input_count"),
// Version::from(1),
// Version::ONE,
// )?,
// height_to_maxfeerate: StorableVec::forced_import(&path.join("height_to_maxfeerate"), Version::from(1))?,
// height_to_medianfeerate: StorableVec::forced_import(&path.join("height_to_medianfeerate"), Version::from(1))?,
// height_to_minfeerate: StorableVec::forced_import(&path.join("height_to_minfeerate"), Version::from(1))?,
// height_to_maxfeerate: StorableVec::forced_import(&path.join("height_to_maxfeerate"), Version::ONE)?,
// height_to_medianfeerate: StorableVec::forced_import(&path.join("height_to_medianfeerate"), Version::ONE)?,
// height_to_minfeerate: StorableVec::forced_import(&path.join("height_to_minfeerate"), Version::ONE)?,
// height_to_output_count: StorableVec::forced_import(
// &path.join("height_to_output_count"),
// Version::from(1),
// Version::ONE,
// )?,
// height_to_subsidy: StorableVec::forced_import(&path.join("height_to_subsidy"), Version::from(1))?,
// height_to_totalfees: StorableVec::forced_import(&path.join("height_to_totalfees"), Version::from(1))?,
// height_to_txcount: StorableVec::forced_import(&path.join("height_to_txcount"), Version::from(1))?,
// height_to_subsidy: StorableVec::forced_import(&path.join("height_to_subsidy"), Version::ONE)?,
// height_to_totalfees: StorableVec::forced_import(&path.join("height_to_totalfees"), Version::ONE)?,
// height_to_txcount: StorableVec::forced_import(&path.join("height_to_txcount"), Version::ONE)?,
// txindex_to_fee: StorableVec::forced_import(
// &path.join("txindex_to_fee"),
// Version::from(1),
// Version::ONE,
// )?,
txindex_to_is_coinbase: StorableVec::forced_import(
&path.join("txindex_to_is_coinbase"),
Version::from(1),
Version::ONE,
compressed,
)?,
// txindex_to_feerate: StorableVec::forced_import(&path.join("txindex_to_feerate"), Version::from(1))?,
// txindex_to_feerate: StorableVec::forced_import(&path.join("txindex_to_feerate"), Version::ONE)?,
txindex_to_inputs_count: StorableVec::forced_import(
&path.join("txindex_to_inputs_count"),
Version::from(1),
Version::ONE,
compressed,
)?,
// txindex_to_inputs_sum: StorableVec::forced_import(
// &path.join("txindex_to_inputs_sum"),
// Version::from(1),
// Version::ONE,
// )?,
txindex_to_outputs_count: StorableVec::forced_import(
&path.join("txindex_to_outputs_count"),
Version::from(1),
Version::ONE,
compressed,
)?,
// txindex_to_outputs_sum: StorableVec::forced_import(
// &path.join("txindex_to_outputs_sum"),
// Version::from(1),
// Version::ONE,
// )?,
// txinindex_to_value: StorableVec::forced_import(
// &path.join("txinindex_to_value"),
// Version::from(1),
// Version::ONE,
// compressed,
// )?,
})
+1 -1
View File
@@ -82,7 +82,7 @@ impl Binance {
}
pub fn get_from_1d(&mut self, date: &Date) -> color_eyre::Result<OHLCCents> {
if self._1d.is_none() || self._1d.as_ref().unwrap().last_key_value().unwrap().0 < date {
if self._1d.is_none() || self._1d.as_ref().unwrap().last_key_value().unwrap().0 <= date {
self._1d.replace(Self::fetch_1d()?);
}
+1 -1
View File
@@ -81,7 +81,7 @@ impl Kibo {
.last_key_value()
.unwrap()
.0
< date
<= date
{
self.year_to_date_to_ohlc
.insert(year, Self::fetch_date_prices(year)?);
+1 -1
View File
@@ -43,7 +43,7 @@ impl Kraken {
}
pub fn get_from_1d(&mut self, date: &Date) -> color_eyre::Result<OHLCCents> {
if self._1d.is_none() || self._1d.as_ref().unwrap().last_key_value().unwrap().0 < date {
if self._1d.is_none() || self._1d.as_ref().unwrap().last_key_value().unwrap().0 <= date {
self._1d.replace(Kraken::fetch_1d()?);
}
self._1d
+7
View File
@@ -27,6 +27,7 @@ pub struct Store<Key, Value> {
}
const CHECK_COLLISISONS: bool = true;
const MAJOR_FJALL_VERSION: Version = Version::TWO;
impl<K, V> Store<K, V>
where
@@ -35,6 +36,8 @@ where
<V as TryFrom<ByteView>>::Error: error::Error + Send + Sync + 'static,
{
pub fn import(path: &Path, version: Version) -> color_eyre::Result<Self> {
let version = MAJOR_FJALL_VERSION + version;
let meta = StoreMeta::checked_open(path, version)?;
let keyspace = match Self::open_keyspace(path) {
@@ -89,6 +92,10 @@ where
}
pub fn remove(&mut self, key: K) {
if self.is_empty() {
return;
}
if !self.puts.is_empty() {
unreachable!("Shouldn't reach this");
// self.puts.remove(&key);
+13 -8
View File
@@ -26,14 +26,12 @@ pub struct Stores {
impl Stores {
pub fn import(path: &Path) -> color_eyre::Result<Self> {
thread::scope(|scope| {
let addresshash_to_addressindex = scope.spawn(|| {
Store::import(&path.join("addresshash_to_addressindex"), Version::from(1))
});
let blockhash_prefix_to_height = scope.spawn(|| {
Store::import(&path.join("blockhash_prefix_to_height"), Version::from(1))
});
let txid_prefix_to_txindex = scope
.spawn(|| Store::import(&path.join("txid_prefix_to_txindex"), Version::from(1)));
let addresshash_to_addressindex = scope
.spawn(|| Store::import(&path.join("addresshash_to_addressindex"), Version::ONE));
let blockhash_prefix_to_height = scope
.spawn(|| Store::import(&path.join("blockhash_prefix_to_height"), Version::ONE));
let txid_prefix_to_txindex =
scope.spawn(|| Store::import(&path.join("txid_prefix_to_txindex"), Version::ONE));
Ok(Self {
addresshash_to_addressindex: addresshash_to_addressindex.join().unwrap()?,
@@ -48,6 +46,13 @@ impl Stores {
vecs: &mut Vecs,
starting_indexes: &Indexes,
) -> color_eyre::Result<()> {
if self.addresshash_to_addressindex.is_empty()
&& self.blockhash_prefix_to_height.is_empty()
&& self.txid_prefix_to_txindex.is_empty()
{
return Ok(());
}
vecs.height_to_blockhash
.iter_from(starting_indexes.height, |(_, blockhash, ..)| {
let blockhash_prefix = BlockHashPrefix::from(blockhash);
+43 -43
View File
@@ -71,217 +71,217 @@ impl Vecs {
Ok(Self {
addressindex_to_addresstype: StorableVec::forced_import(
&path.join("addressindex_to_addresstype"),
Version::from(1),
Version::ONE,
compressed,
)?,
addressindex_to_addresstypeindex: StorableVec::forced_import(
&path.join("addressindex_to_addresstypeindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
addressindex_to_height: StorableVec::forced_import(
&path.join("addressindex_to_height"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_blockhash: StorableVec::forced_import(
&path.join("height_to_blockhash"),
Version::from(1),
Version::ONE,
Compressed::NO,
)?,
height_to_difficulty: StorableVec::forced_import(
&path.join("height_to_difficulty"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_addressindex: StorableVec::forced_import(
&path.join("height_to_first_addressindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_emptyindex: StorableVec::forced_import(
&path.join("height_to_first_emptyindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_multisigindex: StorableVec::forced_import(
&path.join("height_to_first_multisigindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_opreturnindex: StorableVec::forced_import(
&path.join("height_to_first_opreturnindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_pushonlyindex: StorableVec::forced_import(
&path.join("height_to_first_pushonlyindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_txindex: StorableVec::forced_import(
&path.join("height_to_first_txindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_txinindex: StorableVec::forced_import(
&path.join("height_to_first_txinindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_txoutindex: StorableVec::forced_import(
&path.join("height_to_first_txoutindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_unknownindex: StorableVec::forced_import(
&path.join("height_to_first_unkownindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_p2pk33index: StorableVec::forced_import(
&path.join("height_to_first_p2pk33index"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_p2pk65index: StorableVec::forced_import(
&path.join("height_to_first_p2pk65index"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_p2pkhindex: StorableVec::forced_import(
&path.join("height_to_first_p2pkhindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_p2shindex: StorableVec::forced_import(
&path.join("height_to_first_p2shindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_p2trindex: StorableVec::forced_import(
&path.join("height_to_first_p2trindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_p2wpkhindex: StorableVec::forced_import(
&path.join("height_to_first_p2wpkhindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_first_p2wshindex: StorableVec::forced_import(
&path.join("height_to_first_p2wshindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_size: StorableVec::forced_import(
&path.join("height_to_size"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_timestamp: StorableVec::forced_import(
&path.join("height_to_timestamp"),
Version::from(1),
Version::ONE,
compressed,
)?,
height_to_weight: StorableVec::forced_import(
&path.join("height_to_weight"),
Version::from(1),
Version::ONE,
compressed,
)?,
p2pk33index_to_p2pk33addressbytes: StorableVec::forced_import(
&path.join("p2pk33index_to_p2pk33addressbytes"),
Version::from(1),
Version::ONE,
Compressed::NO,
)?,
p2pk65index_to_p2pk65addressbytes: StorableVec::forced_import(
&path.join("p2pk65index_to_p2pk65addressbytes"),
Version::from(1),
Version::ONE,
Compressed::NO,
)?,
p2pkhindex_to_p2pkhaddressbytes: StorableVec::forced_import(
&path.join("p2pkhindex_to_p2pkhaddressbytes"),
Version::from(1),
Version::ONE,
Compressed::NO,
)?,
p2shindex_to_p2shaddressbytes: StorableVec::forced_import(
&path.join("p2shindex_to_p2shaddressbytes"),
Version::from(1),
Version::ONE,
Compressed::NO,
)?,
p2trindex_to_p2traddressbytes: StorableVec::forced_import(
&path.join("p2trindex_to_p2traddressbytes"),
Version::from(1),
Version::ONE,
Compressed::NO,
)?,
p2wpkhindex_to_p2wpkhaddressbytes: StorableVec::forced_import(
&path.join("p2wpkhindex_to_p2wpkhaddressbytes"),
Version::from(1),
Version::ONE,
Compressed::NO,
)?,
p2wshindex_to_p2wshaddressbytes: StorableVec::forced_import(
&path.join("p2wshindex_to_p2wshaddressbytes"),
Version::from(1),
Version::ONE,
Compressed::NO,
)?,
txindex_to_first_txinindex: StorableVec::forced_import(
&path.join("txindex_to_first_txinindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
txindex_to_first_txoutindex: StorableVec::forced_import(
&path.join("txindex_to_first_txoutindex"),
Version::from(1),
Version::ONE,
Compressed::NO,
)?,
txindex_to_height: StorableVec::forced_import(
&path.join("txindex_to_height"),
Version::from(1),
Version::ONE,
compressed,
)?,
txindex_to_locktime: StorableVec::forced_import(
&path.join("txindex_to_locktime"),
Version::from(1),
Version::ONE,
compressed,
)?,
txindex_to_txid: StorableVec::forced_import(
&path.join("txindex_to_txid"),
Version::from(1),
Version::ONE,
Compressed::NO,
)?,
txindex_to_base_size: StorableVec::forced_import(
&path.join("txindex_to_base_size"),
Version::from(1),
Version::ONE,
compressed,
)?,
txindex_to_total_size: StorableVec::forced_import(
&path.join("txindex_to_total_size"),
Version::from(1),
Version::ONE,
compressed,
)?,
txindex_to_is_explicitly_rbf: StorableVec::forced_import(
&path.join("txindex_to_is_explicitly_rbf"),
Version::from(1),
Version::ONE,
compressed,
)?,
txindex_to_txversion: StorableVec::forced_import(
&path.join("txindex_to_txversion"),
Version::from(1),
Version::ONE,
compressed,
)?,
txinindex_to_txoutindex: StorableVec::forced_import(
&path.join("txinindex_to_txoutindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
txoutindex_to_addressindex: StorableVec::forced_import(
&path.join("txoutindex_to_addressindex"),
Version::from(1),
Version::ONE,
compressed,
)?,
txoutindex_to_value: StorableVec::forced_import(
&path.join("txoutindex_to_value"),
Version::from(1),
Version::ONE,
compressed,
)?,
})
+3 -1
View File
@@ -24,5 +24,7 @@ minreq = { workspace = true }
oxc = { version = "0.62.0", features = ["codegen", "minifier"] }
serde = { workspace = true }
tokio = { version = "1.44.1", features = ["full"] }
tower-http = { version = "0.6.2", features = ["compression-full"] }
tower-http = { version = "0.6.2", features = ["compression-full", "trace"] }
zip = "2.5.0"
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
+6 -1
View File
@@ -31,7 +31,12 @@ impl DTS for Query<'static> {
let indexes = Index::all();
let mut contents = indexes
let mut contents = "//
// File auto-generated, any modification will be overwritten
//\n\n"
.to_string();
contents += &indexes
.iter()
.enumerate()
.map(|(i_of_i, i)| {
+3 -15
View File
@@ -1,17 +1,12 @@
use std::time::Instant;
use axum::{
Json,
extract::{Query as AxumQuery, State},
http::{HeaderMap, StatusCode, Uri},
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
};
use brk_query::{Format, Index, Output, Params};
use crate::{
log_result,
traits::{HeaderMapExtended, ModifiedState, ResponseExtended},
};
use crate::traits::{HeaderMapExtended, ModifiedState, ResponseExtended};
use super::AppState;
@@ -21,21 +16,14 @@ pub use dts::*;
pub async fn handler(
headers: HeaderMap,
uri: Uri,
query: AxumQuery<Params>,
State(app_state): State<AppState>,
) -> Response {
let instant = Instant::now();
match req_to_response_res(headers, query, app_state) {
Ok(response) => {
log_result(response.status(), &uri, instant);
response
}
Ok(response) => response,
Err(error) => {
let mut response =
(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response();
log_result(response.status(), &uri, instant);
response.headers_mut().insert_cors();
response
}
+6 -16
View File
@@ -1,15 +1,15 @@
use std::{fs, path::Path, time::Instant};
use std::{fs, path::Path};
use axum::{
body::Body,
extract::{self, State},
http::{HeaderMap, StatusCode, Uri},
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
};
use log::{error, info};
use crate::{
AppState, log_result,
AppState,
traits::{HeaderMapExtended, ModifiedState, ResponseExtended},
};
@@ -18,24 +18,18 @@ use super::minify::minify_js;
pub async fn file_handler(
headers: HeaderMap,
State(app_state): State<AppState>,
uri: Uri,
path: extract::Path<String>,
) -> Response {
any_handler(headers, app_state, uri, Some(path))
any_handler(headers, app_state, Some(path))
}
pub async fn index_handler(
headers: HeaderMap,
State(app_state): State<AppState>,
uri: Uri,
) -> Response {
any_handler(headers, app_state, uri, None)
pub async fn index_handler(headers: HeaderMap, State(app_state): State<AppState>) -> Response {
any_handler(headers, app_state, None)
}
fn any_handler(
headers: HeaderMap,
app_state: AppState,
uri: Uri,
path: Option<extract::Path<String>>,
) -> Response {
let website_path = app_state
@@ -44,8 +38,6 @@ fn any_handler(
.expect("Should never reach here is websites_path is None")
.join(app_state.website.to_folder_name());
let instant = Instant::now();
let response = if let Some(path) = path.as_ref() {
let path = path.0.replace("..", "").replace("\\", "");
@@ -72,8 +64,6 @@ fn any_handler(
path_to_response(&headers, &website_path.join("index.html"))
};
log_result(response.status(), &uri, instant);
response
}
+50 -21
View File
@@ -4,18 +4,18 @@
#![doc = "```"]
use std::{
collections::BTreeMap,
fs,
io::Cursor,
path::{Path, PathBuf},
sync::Arc,
time::Instant,
time::Duration,
};
use api::{ApiRoutes, DTS};
use axum::{
Json, Router,
http::{StatusCode, Uri},
body::Body,
http::{Request, Response, StatusCode, Uri},
middleware::Next,
routing::get,
serve,
};
@@ -28,13 +28,14 @@ use files::FilesRoutes;
use log::{error, info};
pub use tokio;
use tokio::net::TcpListener;
use tower_http::compression::CompressionLayer;
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
mod api;
mod files;
mod traits;
pub use files::Website;
use tracing::Span;
#[derive(Clone)]
pub struct AppState {
@@ -65,14 +66,17 @@ impl Server {
} else {
let downloads_path = dot_brk_path().join(DOWNLOADS);
let downloaded_websites_path = downloads_path.join("brk-main").join(WEBSITES);
let version = format!("v{}", env!("CARGO_PKG_VERSION"));
let downloaded_websites_path = downloads_path.join(&version).join(WEBSITES);
if !fs::exists(&downloaded_websites_path)? {
info!("Downloading websites from Github...");
// TODO: Need to download versioned, main is only for testing
let url =
"https://github.com/bitcoinresearchkit/brk/archive/refs/heads/main.zip";
let url = format!(
"https://github.com/bitcoinresearchkit/brk/archive/refs/tags/{}.zip",
version
);
let response = minreq::get(url).send()?;
let bytes = response.as_bytes();
@@ -109,12 +113,48 @@ impl Server {
.gzip(true)
.zstd(true);
let response_uri_layer = axum::middleware::from_fn(
async |request: Request<Body>, next: Next| -> Response<Body> {
let uri = request.uri().clone();
let mut response = next.run(request).await;
response.extensions_mut().insert(uri);
response
},
);
let trace_layer = TraceLayer::new_for_http()
.on_request(())
.on_response(
|response: &Response<Body>, latency: Duration, _span: &Span| {
let latency = latency.bright_black();
let status = response.status();
let uri = response.extensions().get::<Uri>().unwrap();
match status {
StatusCode::INTERNAL_SERVER_ERROR => {
error!("{} {} {:?}", status.as_u16().red(), uri, latency)
}
StatusCode::NOT_MODIFIED => {
info!("{} {} {:?}", status.as_u16().bright_black(), uri, latency)
}
StatusCode::OK => {
info!("{} {} {:?}", status.as_u16().green(), uri, latency)
}
_ => error!("{} {} {:?}", status.as_u16().red(), uri, latency),
}
},
)
.on_body_chunk(())
.on_failure(())
.on_eos(());
let router = Router::new()
.add_api_routes()
.add_website_routes(state.website)
.route("/version", get(Json(env!("CARGO_PKG_VERSION"))))
.with_state(state)
.layer(compression_layer);
.layer(compression_layer)
.layer(response_uri_layer)
.layer(trace_layer);
let mut port = 3110;
@@ -136,14 +176,3 @@ impl Server {
Ok(())
}
}
pub fn log_result(code: StatusCode, uri: &Uri, instant: Instant) {
let time = format!("{}µs", instant.elapsed().as_micros());
let time = time.bright_black();
match code {
StatusCode::INTERNAL_SERVER_ERROR => error!("{} {} {}", code.as_u16().red(), uri, time),
StatusCode::NOT_MODIFIED => info!("{} {} {}", code.as_u16().bright_black(), uri, time),
StatusCode::OK => info!("{} {} {}", code.as_u16().green(), uri, time),
_ => error!("{} {} {}", code.as_u16().red(), uri, time),
}
}
+7 -8
View File
@@ -17,9 +17,9 @@ pub enum ModifiedState {
}
pub trait HeaderMapExtended {
fn get_scheme(&self) -> &str;
fn _get_scheme(&self) -> &str;
fn get_host(&self) -> &str;
fn check_if_host_is_any_local(&self) -> bool;
fn check_if_host_is_local(&self) -> bool;
fn check_if_host_is_0000(&self) -> bool;
fn check_if_host_is_localhost(&self) -> bool;
@@ -30,8 +30,7 @@ pub trait HeaderMapExtended {
-> color_eyre::Result<(ModifiedState, DateTime)>;
fn insert_cache_control_immutable(&mut self);
#[allow(unused)]
fn insert_cache_control_revalidate(&mut self, max_age: u64, stale_while_revalidate: u64);
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);
@@ -53,8 +52,8 @@ pub trait HeaderMapExtended {
}
impl HeaderMapExtended for HeaderMap {
fn get_scheme(&self) -> &str {
if self.check_if_host_is_any_local() {
fn _get_scheme(&self) -> &str {
if self.check_if_host_is_local() {
"http"
} else {
"https"
@@ -65,7 +64,7 @@ impl HeaderMapExtended for HeaderMap {
self[HOST].to_str().unwrap()
}
fn check_if_host_is_any_local(&self) -> bool {
fn check_if_host_is_local(&self) -> bool {
self.check_if_host_is_localhost() || self.check_if_host_is_0000()
}
@@ -95,7 +94,7 @@ 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) {
fn _insert_cache_control_revalidate(&mut self, max_age: u64, stale_while_revalidate: u64) {
self.insert(
header::CACHE_CONTROL,
format!(
+3 -3
View File
@@ -7,7 +7,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
{
let mut vec: StorableVec<usize, u32> =
StorableVec::forced_import(Path::new("./vec"), Version::from(1), Compressed::YES)?;
StorableVec::forced_import(Path::new("./vec"), Version::ONE, Compressed::YES)?;
(0..21_u32).for_each(|v| {
vec.push(v);
@@ -21,7 +21,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
{
let mut vec: StorableVec<usize, u32> =
StorableVec::forced_import(Path::new("./vec"), Version::from(1), Compressed::YES)?;
StorableVec::forced_import(Path::new("./vec"), Version::ONE, Compressed::YES)?;
dbg!(vec.get(0)?);
dbg!(vec.get(0)?);
@@ -43,7 +43,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
{
let mut vec: StorableVec<usize, u32> =
StorableVec::forced_import(Path::new("./vec"), Version::from(1), Compressed::YES)?;
StorableVec::forced_import(Path::new("./vec"), Version::ONE, Compressed::YES)?;
vec.enable_large_cache();
+4
View File
@@ -200,6 +200,10 @@ where
.to_usize()
.unwrap();
if len == 0 {
return Err(Error::IndexTooHigh);
}
let from = from.map_or(0, |from| {
if from >= 0 {
from as usize
+4
View File
@@ -15,6 +15,10 @@ use crate::{Error, Result};
pub struct Version(u32);
impl Version {
pub const ZERO: Self = Self(0);
pub const ONE: Self = Self(1);
pub const TWO: Self = Self(2);
pub fn write(&self, path: &Path) -> Result<(), io::Error> {
fs::write(path, self.as_bytes())
}
+61 -23
View File
@@ -268,7 +268,6 @@
--negative-main-padding: calc(-1 * var(--main-padding));
--font-weight-base: 400;
--font-weight-bold: 700;
--transform-scale-active: scaleY(0.9);
@@ -284,7 +283,7 @@
@font-face {
font-family: "Geist mono";
src: url("./assets/fonts/geist_mono_var_1_4_01.woff2") format("woff2");
font-weight: 300 500;
font-weight: 100 900;
font-display: block;
font-style: normal;
}
@@ -371,21 +370,13 @@
margin-bottom: 0rem;
}
header {
small {
font-weight: var(--font-weight-base);
font-size: var(--font-size-base);
line-height: var(--line-height-base);
}
}
body > &[hidden] {
display: flex !important;
}
}
b {
font-weight: var(--font-weight-bold);
font-weight: 700;
}
body {
@@ -447,7 +438,8 @@
}
}
h1 {
h1,
h2 {
text-transform: uppercase;
font-size: var(--font-size-2xl);
line-height: var(--line-height-2xl);
@@ -478,6 +470,7 @@
}
input {
text-transform: inherit;
border: 0;
width: 100%;
text-align: left;
@@ -655,7 +648,7 @@
height: 100%;
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: 2rem;
padding-bottom: var(--bottom-area);
}
@@ -928,16 +921,20 @@
gap: 0.5rem;
&[data-size="sm"] {
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
}
&[data-size="xs"] {
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
font-weight: 450;
}
> div.field {
text-transform: lowercase;
display: flex;
align-items: center;
/* font-size: var(--font-size-xs);
line-height: var(--line-height-xs); */
gap: 1rem;
> legend,
@@ -947,6 +944,14 @@
> hr {
min-width: 2rem;
fieldset[data-size="sm"] & {
min-width: 1.5rem;
}
fieldset[data-size="xs"] & {
min-width: 1rem;
}
}
label {
@@ -957,6 +962,14 @@
> div {
display: flex;
gap: 1.5rem;
fieldset[data-size="xs"] & {
gap: 1.25rem;
}
fieldset[data-size="xs"] & {
gap: 1rem;
}
}
}
}
@@ -968,7 +981,7 @@
z-index: 20;
flex: 1;
margin-top: 2rem;
margin-bottom: 1.5rem;
margin-bottom: 1rem;
> legend {
text-transform: lowercase;
@@ -980,9 +993,12 @@
margin-right: var(--negative-main-padding);
padding-left: var(--main-padding);
padding-right: var(--main-padding);
padding-bottom: 1.5rem;
padding-bottom: 1rem;
overflow-x: auto;
min-width: 0;
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
height: 2rem;
> div {
flex: 0;
@@ -1031,6 +1047,17 @@
min-height: 0;
height: 100%;
margin-right: var(--negative-main-padding);
margin-left: var(--negative-main-padding);
fieldset {
padding-left: var(--main-padding);
padding-top: 0.5rem;
z-index: 10;
position: absolute;
left: 0;
top: 0;
gap: 0;
}
}
> .panes {
@@ -1492,20 +1519,31 @@
<div class="shadow-bottom"></div>
<div id="resize-bar"></div>
<nav id="nav" hidden></nav>
<nav id="nav" hidden>
<h4 style="margin-top: 0.25rem">
<a href="/"
><span style="font-weight: 500">kibo</span
><span style="color: var(--gray)">.</span
><span style="color: var(--orange)">money</span></a
>
</h4>
</nav>
<search id="search" hidden>
<header>
<div>
<h3 style="display: flex; flex-direction: column">
<h3
style="
display: flex;
flex-direction: column;
text-transform: uppercase;
"
>
<input placeholder="Search..." id="search-input" />
<small id="search-small">
Focus the title or press <strong>/</strong> to search
</small>
</h3>
</div>
</header>
<ul id="search-results"></ul>
<ul id="search-results" style="text-transform: lowercase"></ul>
</search>
<footer>
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
URL + Version:
https://unpkg.com/browse/lightweight-charts@latest/
@@ -1,117 +0,0 @@
import { Signal } from "../solid-signals/types";
import { Accessor } from "../solid-signals/2024-11-02/types/signals";
import {
DeepPartial,
BaselineStyleOptions,
CandlestickStyleOptions,
LineStyleOptions,
SeriesOptionsCommon,
Time,
ISeriesApi,
BaselineData,
} from "./v5.0.5/types";
import { VecId } from "../../scripts/vecid-to-indexes";
interface BaseSeriesBlueprint {
title: string;
defaultActive?: boolean;
}
interface BaselineSeriesBlueprint extends BaseSeriesBlueprint {
type: "Baseline";
color?: Color;
options?: DeepPartial<BaselineStyleOptions & SeriesOptionsCommon>;
data?: Accessor<BaselineData<Time>[]>;
}
interface CandlestickSeriesBlueprint extends BaseSeriesBlueprint {
type: "Candlestick";
color?: Color;
options?: DeepPartial<CandlestickStyleOptions & SeriesOptionsCommon>;
data?: Accessor<CandlestickData[]>;
}
interface LineSeriesBlueprint extends BaseSeriesBlueprint {
type?: "Line";
color: Color;
options?: DeepPartial<LineStyleOptions & SeriesOptionsCommon>;
data?: Accessor<LineData<Time>[]>;
}
type AnySpecificSeriesBlueprint =
| BaselineSeriesBlueprint
| CandlestickSeriesBlueprint
| LineSeriesBlueprint;
type SeriesType = NonNullable<AnySpecificSeriesBlueprint["type"]>;
type PriceSeriesType = "Candlestick" | "Line";
type RemoveSeriesBlueprintFluff<Blueprint extends AnySpecificSeriesBlueprint> =
Omit<Blueprint, "type" | "title">;
type SplitSeriesBlueprint<> = {
key: VecId;
} & AnySpecificSeriesBlueprint;
type SingleSeriesBlueprint = AnySpecificSeriesBlueprint;
interface CreateBaseSeriesParameters extends BaseSeriesBlueprint {
id: string;
disabled?: Accessor<boolean>;
color?: Color;
}
interface BaseSeries {
id: string;
title: string;
color: Color | Color[];
active: Signal<boolean>;
visible: Accessor<boolean>;
}
interface SingleSeries extends BaseSeries {
iseries: ISeriesApi<SeriesType>;
dataset: Accessor<(SingleValueData | CandlestickData)[] | null>;
}
interface SplitSeries extends BaseSeries {
chunks: Array<Accessor<ISeriesApi<SeriesType> | undefined>>;
// dataset: ResourceDataset<number>;
}
type AnySeries = SingleSeries | SplitSeries;
interface CreateSingleSeriesParameters {
blueprint: SingleSeriesBlueprint;
id: string;
}
interface CreateSplitSeriesParameters {
// dataset: ResourceDataset;
blueprint: SplitSeriesBlueprint;
id: string;
index: number;
disabled?: Accessor<boolean>;
}
type ChartPane = IChartApi & {
whitespace: ISeriesApi<"Line">;
hidden: () => boolean;
setHidden: (b: boolean) => void;
setInitialVisibleTimeRange: VoidFunction;
createSingleSeries: (a: CreateSingleSeriesParameters) => SingleSeries;
createSplitSeries: (a: CreateSplitSeriesParameters) => SplitSeries[];
anySeries: AnySeries[];
singleSeries: SingleSeries[];
splitSeries: SplitSeries[];
remove: VoidFunction;
};
interface CreatePaneParameters {
options?: DeepPartial<ChartOptions>;
config?: SingleSeriesBlueprint[];
}
interface Marker {
weight: number;
time: Time;
value: number;
seriesChunk: ISeriesApi<SeriesType>;
}
interface HoveredLegend {
label: HTMLLabelElement;
series: AnySeries;
}
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 one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,28 @@
// @ts-check
/** @import {ISeriesApi, SeriesDefinition} from './v5.0.5/types' */
/** @import {IChartApi, ISeriesApi, SeriesDefinition, SingleValueData as _SingleValueData, CandlestickData as _CandlestickData, BaselineData, SeriesType} from './v5.0.5-treeshaked/types' */
export default import("./v5.0.5/script.js").then((lc) => {
/**
* @typedef {[number, number, number, number]} OHLCTuple
*
* @typedef {Object} Valued
* @property {number} value
*
* @typedef {Object} Indexed
* @property {number} index
*/
/**
* @template T
* @typedef {T & Valued & Indexed} ChartData<T>
*/
/**
* @typedef {ChartData<_SingleValueData>} SingleValueData
* @typedef {ChartData<_CandlestickData>} CandlestickData
*/
export default import("./v5.0.5-treeshaked/script.js").then((lc) => {
const oklchToRGBA = createOklchToRGBA();
/**
@@ -27,7 +47,7 @@ export default import("./v5.0.5/script.js").then((lc) => {
autoSize: true,
layout: {
fontFamily: "Geist mono",
fontSize: 14,
fontSize: 13,
background: { color: "transparent" },
attributionLogo: false,
colorSpace: "display-p3",
@@ -51,11 +71,11 @@ export default import("./v5.0.5/script.js").then((lc) => {
const chart = lc.createChart(element, options);
chart.priceScale("right").applyOptions({
scaleMargins: {
top: 0.075,
bottom: 0.05,
},
minimumWidth: 78,
// scaleMargins: {
// top: 0.15,
// bottom: 0.05,
// },
minimumWidth: 80,
});
signals.createEffect(
@@ -110,15 +130,18 @@ export default import("./v5.0.5/script.js").then((lc) => {
* @param {VecsResources} args.vecsResources
* @param {Owner | null} [args.owner]
* @param {true} [args.fitContentOnResize]
* @param {{unit: Unit; blueprints: AnySeriesBlueprint[]}[]} [args.config]
*/
function createChartElement({
parent,
signals,
colors,
utils,
id,
vecsResources,
owner: _owner,
fitContentOnResize,
config,
}) {
let owner = _owner || signals.getOwner();
@@ -150,13 +173,16 @@ export default import("./v5.0.5/script.js").then((lc) => {
let timeResource = /** @type {VecResource| null} */ (null);
let timeScaleSetCallback = /** @type {VoidFunction | null} */ (null);
let timeScaleSetCallback =
/** @type {((unknownTimeScaleCallback: VoidFunction) => void) | null} */ (
null
);
/**
* @param {ISeriesApi<SeriesType>} series
* @param {VecResource} valuesResource
*/
function createSetDataEffect(series, valuesResource) {
function createSetFetchedDataEffect(series, valuesResource) {
signals.runWithOwner(owner, () =>
signals.createEffect(
() => [timeResource?.fetched(), valuesResource.fetched()],
@@ -193,17 +219,18 @@ export default import("./v5.0.5/script.js").then((lc) => {
}
data.length -= offset;
series.setData(data);
timeScaleSetCallback?.();
if (
!timeScaleSet &&
(vecIndex === /** @satisfies {Quarterindex} */ (5) ||
vecIndex === /** @satisfies {Yearindex} */ (6) ||
vecIndex === /** @satisfies {Decadeindex} */ (7))
) {
ichart
.timeScale()
.setVisibleLogicalRange({ from: -1, to: data.length });
}
timeScaleSetCallback?.(() => {
if (
!timeScaleSet &&
(vecIndex === /** @satisfies {Quarterindex} */ (5) ||
vecIndex === /** @satisfies {Yearindex} */ (6) ||
vecIndex === /** @satisfies {Decadeindex} */ (7))
) {
ichart
?.timeScale()
.setVisibleLogicalRange({ from: -1, to: data.length });
}
});
timeScaleSet = true;
},
),
@@ -220,12 +247,12 @@ export default import("./v5.0.5/script.js").then((lc) => {
}),
);
return {
const chart = {
inner: () => ichart,
/**
* @param {Object} args
* @param {Index} args.index
* @param {VoidFunction} [args.timeScaleSetCallback]
* @param {((unknownTimeScaleCallback: VoidFunction) => void)} [args.timeScaleSetCallback]
*/
create({ index: _index, timeScaleSetCallback: _timeScaleSetCallback }) {
vecIndex = _index;
@@ -248,20 +275,37 @@ export default import("./v5.0.5/script.js").then((lc) => {
colors,
utils,
});
if (fitContentOnResize) {
ichart.applyOptions({
handleScroll: false,
handleScale: false,
timeScale: {
minBarSpacing: 0.001,
},
});
}
},
/**
* @param {Object} args
* @param {VecId} args.vecId
* @param {string} args.name
* @param {number} [args.paneNumber]
* @param {Unit} args.unit
* @param {VecId} [args.vecId]
* @param {Accessor<CandlestickData[]>} [args.data]
* @param {number} [args.paneIndex]
* @param {boolean} [args.defaultActive]
*/
addCandlestickSeries({ vecId, name, paneNumber, defaultActive }) {
if (!ichart || !timeResource) throw Error("Chart not fully set");
addCandlestickSeries({
vecId,
name,
unit,
paneIndex: _paneIndex,
defaultActive,
data,
}) {
const paneIndex = _paneIndex ?? 0;
const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
valuesResource.fetch();
activeResources.push(valuesResource);
if (!ichart || !timeResource) throw Error("Chart not fully set");
const green = colors.green();
const red = colors.red();
@@ -275,35 +319,75 @@ export default import("./v5.0.5/script.js").then((lc) => {
borderVisible: false,
visible: defaultActive !== false,
},
paneNumber,
paneIndex,
);
let url = /** @type {string | undefined} */ (undefined);
if (vecId) {
const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
valuesResource.fetch();
activeResources.push(valuesResource);
createSetFetchedDataEffect(series, valuesResource);
url = valuesResource.url;
} else if (data) {
signals.runWithOwner(owner, () =>
signals.createEffect(data, (data) => {
series.setData(data);
}),
);
}
legend.add({
series,
name,
defaultActive,
colors: [colors.green, colors.red],
url: valuesResource.url,
url,
});
createSetDataEffect(series, valuesResource);
createPaneHeightObserver({
ichart,
paneIndex,
signals,
utils,
});
createPriceScaleSelectorIfNeeded({
ichart,
paneIndex,
seriesType: "Candlestick",
signals,
id,
unit,
utils,
});
return series;
},
/**
* @param {Object} args
* @param {VecId} args.vecId
* @param {string} args.name
* @param {Unit} args.unit
* @param {Accessor<LineData[]>} [args.data]
* @param {VecId} [args.vecId]
* @param {Color} [args.color]
* @param {number} [args.paneNumber]
* @param {number} [args.paneIndex]
* @param {boolean} [args.defaultActive]
*/
addLineSeries({ vecId, name, color, paneNumber, defaultActive }) {
addLineSeries({
vecId,
name,
unit,
color,
paneIndex: _paneIndex,
defaultActive,
data,
}) {
if (!ichart || !timeResource) throw Error("Chart not fully set");
const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
valuesResource.fetch();
activeResources.push(valuesResource);
const paneIndex = _paneIndex ?? 0;
color ||= colors.orange;
@@ -315,18 +399,139 @@ export default import("./v5.0.5/script.js").then((lc) => {
priceLineVisible: false,
color: color(),
},
paneNumber,
paneIndex,
);
let url = /** @type {string | undefined} */ (undefined);
if (vecId) {
const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
valuesResource.fetch();
activeResources.push(valuesResource);
createSetFetchedDataEffect(series, valuesResource);
url = valuesResource.url;
} else if (data) {
signals.runWithOwner(owner, () =>
signals.createEffect(data, (data) => {
series.setData(data);
ichart
?.timeScale()
.setVisibleLogicalRange({ from: -1, to: data.length });
}),
);
}
legend.add({
series,
colors: [color],
name,
defaultActive,
url: valuesResource.url,
url,
});
createSetDataEffect(series, valuesResource);
createPaneHeightObserver({
ichart,
paneIndex,
signals,
utils,
});
createPriceScaleSelectorIfNeeded({
ichart,
paneIndex,
signals,
seriesType: "Line",
id,
unit,
utils,
});
return series;
},
/**
* @param {Object} args
* @param {string} args.name
* @param {Unit} args.unit
* @param {Accessor<BaselineData[]>} [args.data]
* @param {VecId} [args.vecId]
* @param {number} [args.paneIndex]
* @param {boolean} [args.defaultActive]
*/
addBaselineSeries({
vecId,
name,
unit,
paneIndex: _paneIndex,
defaultActive,
data,
}) {
if (!ichart || !timeResource) throw Error("Chart not fully set");
const paneIndex = _paneIndex ?? 0;
const series = ichart.addSeries(
/** @type {SeriesDefinition<'Baseline'>} */ (lc.BaselineSeries),
{
lineWidth: /** @type {any} */ (1.5),
visible: defaultActive !== false,
topLineColor: colors.green(),
bottomLineColor: colors.red(),
baseValue: {
price: 0,
},
baseLineStyle: 0,
baseLineWidth: 1,
baseLineVisible: true,
lineVisible: true,
},
paneIndex,
);
let url = /** @type {string | undefined} */ (undefined);
if (vecId) {
const valuesResource = vecsResources.getOrCreate(vecIndex, vecId);
valuesResource.fetch();
activeResources.push(valuesResource);
createSetFetchedDataEffect(series, valuesResource);
url = valuesResource.url;
} else if (data) {
signals.runWithOwner(owner, () =>
signals.createEffect(data, (data) => {
series.setData(data);
ichart
?.timeScale()
.setVisibleLogicalRange({ from: -1, to: data.length });
}),
);
}
legend.add({
series,
colors: [colors.green, colors.red],
name,
defaultActive,
url,
});
createPaneHeightObserver({
ichart,
paneIndex,
signals,
utils,
});
createPriceScaleSelectorIfNeeded({
ichart,
paneIndex,
signals,
seriesType: "Baseline",
id,
unit,
utils,
});
return series;
},
@@ -344,6 +549,41 @@ export default import("./v5.0.5/script.js").then((lc) => {
legend.reset();
},
};
config?.forEach(({ unit, blueprints }, paneIndex) => {
chart.create({ index: /** @satisfies {Dateindex} */ (1) });
blueprints.forEach((blueprint) => {
if (blueprint.type === "Candlestick") {
chart.addCandlestickSeries({
name: blueprint.title,
unit,
data: blueprint.data,
defaultActive: blueprint.defaultActive,
paneIndex,
});
} else if (blueprint.type === "Baseline") {
chart.addBaselineSeries({
name: blueprint.title,
unit,
data: blueprint.data,
defaultActive: blueprint.defaultActive,
paneIndex,
});
} else {
chart.addLineSeries({
name: blueprint.title,
unit,
data: blueprint.data,
defaultActive: blueprint.defaultActive,
paneIndex,
color: blueprint.color,
});
}
});
});
return chart;
}
return {
@@ -600,3 +840,163 @@ function createOklchToRGBA() {
};
}
}
/**
* @param {Object} args
* @param {IChartApi} args.ichart
* @param {number} args.paneIndex
* @param {Signals} args.signals
* @param {Utilities} args.utils
*/
function createPaneHeightObserver({ ichart, paneIndex, signals, utils }) {
if (!paneIndex) return;
const owner = signals.getOwner();
const one = "1";
const callback = () =>
setTimeout(() => {
try {
const _element = ichart?.panes().at(paneIndex)?.getHTMLElement();
if (!_element) return callback();
const element = _element;
if (element.dataset.observed === one) return;
element.dataset.observed = one;
signals.runWithOwner(owner, () => {
const height = signals.createSignal(null, {
save: {
keyPrefix: "charts",
key: `height-${paneIndex}`,
...utils.serde.optNumber,
},
});
const savedHeight = height();
if (savedHeight !== null) {
ichart.panes().at(paneIndex)?.setHeight(savedHeight);
}
let firstRun = true;
new ResizeObserver(() => {
if (firstRun && savedHeight !== null) {
firstRun = false;
} else {
const h = ichart.panes().at(paneIndex)?.getHeight();
if (h === undefined) return;
height.set(h);
}
}).observe(element);
});
} catch {
callback();
}
}, 5);
callback();
}
/**
* @param {Object} args
* @param {IChartApi} args.ichart
* @param {Unit} args.unit
* @param {string} args.id
* @param {SeriesType} args.seriesType
* @param {number} args.paneIndex
* @param {Signals} args.signals
* @param {Utilities} args.utils
*/
function createPriceScaleSelectorIfNeeded({
ichart,
unit,
paneIndex,
id,
seriesType,
signals,
utils,
}) {
const owner = signals.getOwner();
setTimeout(
() => {
const parent = ichart
?.panes()
.at(paneIndex)
?.getHTMLElement()
.children?.item(1)?.firstChild;
if (!parent) throw Error("Parent should exist");
const tagName = "fieldset";
if (parent.lastChild?.nodeName.toLowerCase() === tagName) {
return;
}
console.log(id);
const choices = /**@type {const} */ (["lin", "log"]);
/** @typedef {(typeof choices)[number]} Choices */
const serializedValue = signals.createSignal(
/** @satisfies {Choices} */ (
unit === "US Dollars" && seriesType !== "Baseline" ? "log" : "lin"
),
{
save: {
...utils.serde.string,
keyPrefix: "",
key: `${id}-price-scale-${paneIndex}`,
},
},
);
const field = utils.dom.createHorizontalChoiceField({
title: unit,
selected: serializedValue(),
choices: choices,
id: `${id}-${unit.replace(" ", "-")}`,
signals,
});
field.addEventListener("change", (event) => {
// @ts-ignore
const value = event.target.value;
serializedValue.set(value);
});
const element = window.document.createElement(tagName);
element.dataset.size = "xs";
element.id = `${id}-price-scale-${paneIndex}`;
element.append(field);
const mode = signals.createMemo(() => {
switch (serializedValue()) {
case "lin":
return 0;
case "log":
return 1;
}
});
const pane = ichart?.panes().at(paneIndex);
if (!pane) throw Error("Expect pane");
signals.runWithOwner(owner, () => {
signals.createEffect(mode, (mode) => {
try {
pane.priceScale("right").applyOptions({
mode,
});
} catch {}
});
});
pane.getHTMLElement().children?.item(1)?.firstChild?.appendChild(element);
},
paneIndex ? 10 : 0,
);
}
@@ -1,790 +0,0 @@
// src/core/error.ts
var NotReadyError = class extends Error {
};
var NoOwnerError = class extends Error {
constructor() {
super(
""
);
}
};
var ContextNotFoundError = class extends Error {
constructor() {
super(
""
);
}
};
// src/utils.ts
function isUndefined(value) {
return typeof value === "undefined";
}
// src/core/constants.ts
var STATE_CLEAN = 0;
var STATE_CHECK = 1;
var STATE_DIRTY = 2;
var STATE_DISPOSED = 3;
// 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
l = null;
g = null;
j = null;
a = STATE_CLEAN;
e = null;
h = defaultContext;
f = null;
constructor(signal = false) {
if (currentOwner && !signal)
currentOwner.append(this);
}
append(child) {
child.l = this;
child.j = this;
if (this.g)
this.g.j = child;
child.g = this.g;
this.g = child;
if (child.h !== this.h) {
child.h = { ...this.h, ...child.h };
}
if (this.f) {
child.f = !child.f ? this.f : [...child.f, ...this.f];
}
}
dispose(self = true) {
if (this.a === STATE_DISPOSED)
return;
let head = self ? this.j || this.l : this, current = this.g, next = null;
while (current && current.l === this) {
current.dispose(true);
current.n();
next = current.g;
current.g = null;
current = next;
}
if (self)
this.n();
if (current)
current.j = !self ? this : this.j;
if (head)
head.g = current;
}
n() {
if (this.j)
this.j.g = null;
this.l = null;
this.j = null;
this.h = defaultContext;
this.f = null;
this.a = STATE_DISPOSED;
this.emptyDisposal();
}
emptyDisposal() {
if (!this.e)
return;
if (Array.isArray(this.e)) {
for (let i = 0; i < this.e.length; i++) {
const callable = this.e[i];
callable.call(callable);
}
} else {
this.e.call(this.e);
}
this.e = null;
}
handleError(error) {
if (!this.f)
throw error;
let i = 0, len = this.f.length;
for (i = 0; i < len; i++) {
try {
this.f[i](error);
break;
} catch (e) {
error = e;
}
}
if (i === len)
throw error;
}
};
function createContext(defaultValue, description) {
return { id: Symbol(description), defaultValue };
}
function getContext(context, owner = currentOwner) {
if (!owner) {
throw new NoOwnerError();
}
const value = hasContext(context, owner) ? owner.h[context.id] : context.defaultValue;
if (isUndefined(value)) {
throw new ContextNotFoundError();
}
return value;
}
function setContext(context, value, owner = currentOwner) {
if (!owner) {
throw new NoOwnerError();
}
owner.h = {
...owner.h,
[context.id]: isUndefined(value) ? context.defaultValue : value
};
}
function hasContext(context, owner = currentOwner) {
return !isUndefined(owner?.h[context.id]);
}
function onCleanup(disposable) {
if (!currentOwner)
return;
const node = currentOwner;
if (!node.e) {
node.e = disposable;
} else if (Array.isArray(node.e)) {
node.e.push(disposable);
} else {
node.e = [node.e, disposable];
}
}
// 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 DEFAULT_FLAGS = ERROR_BIT;
// src/core/scheduler.ts
var scheduled = false;
var runningScheduled = false;
var Computations = [];
var RenderEffects = [];
var Effects = [];
function flushSync() {
if (!runningScheduled)
runScheduled();
}
function flushQueue() {
if (scheduled)
return;
scheduled = true;
queueMicrotask(runScheduled);
}
function runTop(node) {
const ancestors = [];
for (let current = node; current !== null; current = current.l) {
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].m();
}
}
function runScheduled() {
if (!Effects.length && !RenderEffects.length && !Computations.length) {
scheduled = false;
return;
}
runningScheduled = true;
try {
runPureQueue(Computations);
runPureQueue(RenderEffects);
runPureQueue(Effects);
} finally {
const renderEffects = RenderEffects;
const effects = Effects;
Computations = [];
Effects = [];
RenderEffects = [];
scheduled = false;
runningScheduled = false;
incrementClock();
runEffectQueue(renderEffects);
runEffectQueue(effects);
}
}
function runPureQueue(queue) {
for (let i = 0; i < queue.length; i++) {
if (queue[i].a !== STATE_CLEAN)
runTop(queue[i]);
}
}
function runEffectQueue(queue) {
for (let i = 0; i < queue.length; i++) {
if (queue[i].q && queue[i].a !== STATE_DISPOSED) {
queue[i].r(queue[i].d, queue[i].o);
queue[i].q = false;
queue[i].o = queue[i].d;
}
}
}
// src/core/core.ts
var currentObserver = null;
var currentMask = DEFAULT_FLAGS;
var newSources = null;
var newSourcesIndex = 0;
var newFlags = 0;
var clock = 0;
var syncResolve = false;
var updateCheck = null;
function getObserver() {
return currentObserver;
}
function incrementClock() {
clock++;
}
var UNCHANGED = Symbol(0);
var Computation2 = class extends Owner {
b = null;
c = null;
d;
s;
// Used in __DEV__ mode, hopefully removed in production
B;
// Using false is an optimization as an alternative to _equals: () => false
// which could enable more efficient DIRTY notification
t = isEqual;
x;
/** Whether the computation is an error or has ancestors that are unresolved */
i = 0;
/** Which flags raised by sources are handled, vs. being passed through. */
p = DEFAULT_FLAGS;
u = null;
v = null;
w = -1;
constructor(initialValue, compute2, options) {
super(compute2 === null);
this.s = compute2;
this.a = compute2 ? STATE_DIRTY : STATE_CLEAN;
this.d = initialValue;
if (options?.equals !== void 0)
this.t = options.equals;
if (options?.unobserved)
this.x = options?.unobserved;
}
y() {
if (this.s)
this.m();
if (!this.b || this.b.length)
track(this);
newFlags |= this.i & ~currentMask;
if (this.i & ERROR_BIT) {
throw this.d;
} else {
return this.d;
}
}
/**
* Return the current value of this computation
* Automatically re-executes the surrounding computation when the value changes
*/
read() {
return this.y();
}
/**
* 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 (!syncResolve && this.loading()) {
throw new NotReadyError();
}
return this.y();
}
/**
* Return true if the computation is the value is dependent on an unresolved promise
* Triggers re-execution of the computation when the loading state changes
*
* This is useful especially when effects want to re-execute when a computation's
* loading state changes
*/
loading() {
if (this.v === null) {
this.v = loadingState(this);
}
return this.v.read();
}
/**
* Return true if the computation is the computation threw an error
* Triggers re-execution of the computation when the error state changes
*/
error() {
if (this.u === null) {
this.u = errorState(this);
}
return this.u.read();
}
/** Update the computation with a new value. */
write(value, flags = 0, raw = false) {
const newValue = !raw && typeof value === "function" ? value(this.d) : value;
const valueChanged = newValue !== UNCHANGED && (!!(flags & ERROR_BIT) || this.t === false || !this.t(this.d, newValue));
if (valueChanged)
this.d = newValue;
const changedFlagsMask = this.i ^ flags, changedFlags = changedFlagsMask & flags;
this.i = flags;
this.w = clock + 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].z(changedFlagsMask, changedFlags);
}
}
}
return this.d;
}
/**
* Set the current node's state, and recursively mark all of this node's observers as STATE_CHECK
*/
k(state) {
if (this.a >= state)
return;
this.a = state;
if (this.c) {
for (let i = 0; i < this.c.length; i++) {
this.c[i].k(STATE_CHECK);
}
}
}
/**
* 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.
*/
z(mask, newFlags2) {
if (this.a >= STATE_DIRTY)
return;
if (mask & this.p) {
this.k(STATE_DIRTY);
return;
}
if (this.a >= STATE_CHECK)
return;
const prevFlags = this.i & mask;
const deltaFlags = prevFlags ^ newFlags2;
if (newFlags2 === prevFlags) ; else if (deltaFlags & prevFlags & mask) {
this.k(STATE_CHECK);
} else {
this.i ^= deltaFlags;
if (this.c) {
for (let i = 0; i < this.c.length; i++) {
this.c[i].z(mask, newFlags2);
}
}
}
}
A(error) {
this.write(error, this.i | ERROR_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
*/
m() {
if (this.a === STATE_DISPOSED) {
throw new Error("Tried to read a disposed computation");
}
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].m();
observerFlags |= this.b[i].i;
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
*/
n() {
if (this.a === STATE_DISPOSED)
return;
if (this.b)
removeSourceObservers(this, 0);
super.n();
}
};
function loadingState(node) {
const prevOwner = setOwner(node.l);
const options = void 0;
const computation = new Computation2(
void 0,
() => {
track(node);
node.m();
return !!(node.i & LOADING_BIT);
},
options
);
computation.p = ERROR_BIT | LOADING_BIT;
setOwner(prevOwner);
return computation;
}
function errorState(node) {
const prevOwner = setOwner(node.l);
const options = void 0;
const computation = new Computation2(
void 0,
() => {
track(node);
node.m();
return !!(node.i & ERROR_BIT);
},
options
);
computation.p = ERROR_BIT;
setOwner(prevOwner);
return computation;
}
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);
}
if (updateCheck) {
updateCheck.d = computation.w > currentObserver.w;
}
}
}
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.s, node);
node.write(result, newFlags, true);
} catch (error) {
if (error instanceof NotReadyError) {
node.write(UNCHANGED, newFlags | LOADING_BIT);
} else {
node.A(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.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.x?.();
}
}
}
function isEqual(a, b) {
return a === b;
}
function untrack(fn) {
if (currentObserver === null)
return fn();
return compute(getOwner(), fn, null);
}
function hasUpdated(fn) {
const current = updateCheck;
updateCheck = { d: false };
try {
fn();
return updateCheck.d;
} finally {
updateCheck = current;
}
}
function isPending(fn) {
try {
fn();
return false;
} catch (e) {
return e instanceof NotReadyError;
}
}
function latest(fn) {
const prevFlags = newFlags;
syncResolve = true;
try {
return fn();
} catch {
} finally {
newFlags = prevFlags;
syncResolve = false;
}
}
function compute(owner, compute2, observer) {
const prevOwner = setOwner(owner), prevObserver = currentObserver, prevMask = currentMask;
currentObserver = observer;
currentMask = observer?.p ?? DEFAULT_FLAGS;
try {
return compute2(observer ? observer.d : void 0);
} finally {
setOwner(prevOwner);
currentObserver = prevObserver;
currentMask = prevMask;
}
}
var EagerComputation = class extends Computation2 {
constructor(initialValue, compute2, options) {
super(initialValue, compute2, options);
this.m();
Computations.push(this);
}
k(state) {
if (this.a >= state)
return;
if (this.a === STATE_CLEAN) {
Computations.push(this);
flushQueue();
}
super.k(state);
}
};
// src/core/effect.ts
var BaseEffect = class extends Computation2 {
r;
q = false;
o;
constructor(initialValue, compute2, effect, options) {
super(initialValue, compute2, options);
this.r = effect;
this.o = initialValue;
}
write(value) {
if (value === UNCHANGED)
return this.d;
this.d = value;
this.q = true;
return value;
}
A(error) {
this.handleError(error);
}
n() {
this.r = void 0;
this.o = void 0;
super.n();
}
};
var Effect = class extends BaseEffect {
constructor(initialValue, compute2, effect, options) {
super(initialValue, compute2, effect, options);
Effects.push(this);
flushQueue();
}
k(state) {
if (this.a >= state)
return;
if (this.a === STATE_CLEAN) {
Effects.push(this);
flushQueue();
}
this.a = state;
}
};
var RenderEffect = class extends BaseEffect {
constructor(initialValue, compute2, effect, options) {
super(initialValue, compute2, effect, options);
this.m();
RenderEffects.push(this);
}
k(state) {
if (this.a >= state)
return;
if (this.a === STATE_CLEAN) {
RenderEffects.push(this);
flushQueue();
}
this.a = state;
}
};
// src/signals.ts
function createSignal(first, second, third) {
if (typeof first === "function") {
const memo = createMemo((p) => {
const node2 = new Computation2(
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 node = new Computation2(first, null, second);
return [node.read.bind(node), node.write.bind(node)];
}
function createAsync(fn, initial, options) {
const lhs = new EagerComputation(
{
d: initial
},
(p) => {
const value = p?.d;
const source = fn(value);
const isPromise = source instanceof Promise;
const iterator = source[Symbol.asyncIterator];
if (!isPromise && !iterator) {
return {
wait() {
return source;
},
d: source
};
}
const signal = new Computation2(value, null, options);
signal.write(UNCHANGED, LOADING_BIT);
if (isPromise) {
source.then(
(value2) => {
signal.write(value2, 0);
},
(error) => {
signal.write(error, ERROR_BIT);
}
);
} else {
let abort = false;
onCleanup(() => abort = true);
(async () => {
try {
for await (let value2 of source) {
if (abort)
return;
signal.write(value2, 0);
}
} catch (error) {
signal.write(error, ERROR_BIT);
}
})();
}
return signal;
}
);
return () => lhs.wait().wait();
}
function createMemo(compute2, initialValue, options) {
let node = new Computation2(initialValue, compute2, options);
let value;
return () => {
if (node) {
value = node.wait();
if (!node.b?.length)
node = void 0;
}
return value;
};
}
function createEffect(compute2, effect, initialValue, options) {
void new Effect(
initialValue,
compute2,
effect,
void 0
);
}
function createRenderEffect(compute2, effect, initialValue, options) {
void new RenderEffect(
initialValue,
compute2,
effect,
void 0
);
}
function createRoot(init) {
const owner = new Owner();
return compute(owner, !init.length ? init : () => init(() => owner.dispose()), null);
}
function runWithOwner(owner, run) {
try {
return compute(owner, run, null);
} catch (error) {
owner?.handleError(error);
return void 0;
}
}
function catchError(fn, handler) {
const owner = new Owner();
owner.f = owner.f ? [handler, ...owner.f] : [handler];
try {
compute(owner, fn, null);
} catch (error) {
owner.handleError(error);
}
}
export { Computation2 as Computation, ContextNotFoundError, NoOwnerError, NotReadyError, Owner, catchError, createAsync, createContext, createEffect, createMemo, createRenderEffect, createRoot, createSignal, flushSync, getContext, getObserver, getOwner, hasContext, hasUpdated, isEqual, isPending, latest, onCleanup, runWithOwner, setContext, untrack };
@@ -1,23 +0,0 @@
import { Computation, type SignalOptions } from "./core.js";
/**
* Effects are the leaf nodes of our reactive graph. When their sources change, they are
* automatically added to the queue of effects to re-execute, which will cause them to fetch their
* sources and recompute
*/
export declare class BaseEffect<T = any> extends Computation<T> {
_effect: (val: T, prev: T | undefined) => void;
_modified: boolean;
_prevValue: T | undefined;
constructor(initialValue: T, compute: () => T, effect: (val: T, prev: T | undefined) => void, options?: SignalOptions<T>);
write(value: T): T;
_setError(error: unknown): void;
_disposeNode(): void;
}
export declare class Effect<T = any> extends BaseEffect<T> {
constructor(initialValue: T, compute: () => T, effect: (val: T, prev: T | undefined) => void, options?: SignalOptions<T>);
_notify(state: number): void;
}
export declare class RenderEffect<T = any> extends BaseEffect<T> {
constructor(initialValue: T, compute: () => T, effect: (val: T, prev: T | undefined) => void, options?: SignalOptions<T>);
_notify(state: number): void;
}
@@ -1,6 +0,0 @@
export { ContextNotFoundError, NoOwnerError, NotReadyError, type ErrorHandler } from "./error.js";
export { Owner, createContext, getContext, setContext, hasContext, getOwner, onCleanup, type Context, type ContextRecord, type Disposable } from "./owner.js";
export { Computation, EagerComputation, getObserver, isEqual, untrack, hasUpdated, isPending, latest, UNCHANGED, compute, type SignalOptions } from "./core.js";
export { Effect, RenderEffect } from "./effect.js";
export { flushSync } from "./scheduler.js";
export * from "./flags.js";
@@ -1,9 +0,0 @@
import type { Effect, RenderEffect } from "./effect.js";
import { Computation } from "./core.js";
export declare let Computations: Computation[], RenderEffects: RenderEffect[], Effects: Effect[];
/**
* By default, changes are batched on the microtask queue which is an async process. You can flush
* the queue synchronously to get the latest updates by calling `flushSync()`.
*/
export declare function flushSync(): void;
export declare function flushQueue(): void;
@@ -1,4 +0,0 @@
export { Computation, ContextNotFoundError, NoOwnerError, NotReadyError, Owner, createContext, getContext, setContext, hasContext, getOwner, onCleanup, getObserver, isEqual, untrack, hasUpdated, isPending, latest } from "./core/index.js";
export type { ErrorHandler, SignalOptions, Context, ContextRecord, Disposable } from "./core/index.js";
export { flushSync } from "./core/scheduler.js";
export * from "./signals.js";
@@ -1,12 +0,0 @@
import type { Accessor } from "./signals.js";
export type Maybe<T> = T | void | null | undefined | false;
/**
* Reactive map helper that caches each list item by reference to reduce unnecessary mapping on
* updates.
*
* @see {@link https://github.com/solidjs/x-reactivity#maparray}
*/
export declare function mapArray<Item, MappedItem>(list: Accessor<Maybe<readonly Item[]>>, map: (value: Accessor<Item>, index: Accessor<number>) => MappedItem, options?: {
keyed?: boolean | ((item: Item) => any);
name?: string;
}): Accessor<MappedItem[]>;
@@ -1,56 +0,0 @@
import type { SignalOptions } from "./core/index.js";
import { Owner } from "./core/index.js";
export interface Accessor<T> {
(): T;
}
export interface Setter<T> {
(value: T | SetValue<T>): T;
}
export interface SetValue<T> {
(currentValue: T): T;
}
export type Signal<T> = [read: Accessor<T>, write: Setter<T>];
/**
* Wraps the given value into a signal. The signal will return the current value when invoked
* `fn()`, and provide a simple write API via `write()`. The value can now be observed
* when used inside other computations created with `computed` and `effect`.
*/
export declare function createSignal<T>(initialValue: Exclude<T, Function>, options?: SignalOptions<T>): Signal<T>;
export declare function createSignal<T>(fn: (prev?: T) => T, initialValue?: T, options?: SignalOptions<T>): Signal<T>;
export declare function createAsync<T>(fn: (prev?: T) => Promise<T> | AsyncIterable<T> | T, initial?: T, options?: SignalOptions<T>): Accessor<T>;
/**
* Creates a new computation whose value is computed and returned by the given function. The given
* compute function is _only_ re-run when one of it's dependencies are updated. Dependencies are
* are all signals that are read during execution.
*/
export declare function createMemo<T>(compute: (prev?: T) => T, initialValue?: T, options?: SignalOptions<T>): Accessor<T>;
/**
* Invokes the given function each time any of the signals that are read inside are updated
* (i.e., their value changes). The effect is immediately invoked on initialization.
*/
export declare function createEffect<T>(compute: () => T, effect: (v: T) => (() => void) | void, initialValue?: T, options?: {
name?: string;
}): void;
/**
* Invokes the given function each time any of the signals that are read inside are updated
* (i.e., their value changes). The effect is immediately invoked on initialization.
*/
export declare function createRenderEffect<T>(compute: () => T, effect: (v: T) => (() => void) | void, initialValue?: T, options?: {
name?: string;
}): void;
/**
* Creates a computation root which is given a `dispose()` function to dispose of all inner
* computations.
*/
export declare function createRoot<T>(init: ((dispose: () => void) => T) | (() => T)): T;
/**
* Runs the given function in the given owner so that error handling and cleanups continue to work.
*
* Warning: Usually there are simpler ways of modeling a problem that avoid using this function
*/
export declare function runWithOwner<T>(owner: Owner | null, run: () => T): T | undefined;
/**
* Runs the given function when an error is thrown in a child owner. If the error is thrown again
* inside the error handler, it will trigger the next available parent owner handler.
*/
export declare function catchError<T>(fn: () => T, handler: (error: unknown) => void): void;
@@ -1 +0,0 @@
export * from "./store.js";
@@ -1,5 +0,0 @@
Compiled version of: https://github.com/solidjs/signals/commits/main/
Head:
- SHA: 4d75d3f84ce22b560988f3b27a5065c0fd2e69a8
- Date: Apr 17, 2024
+1 -1
View File
@@ -1,4 +1,4 @@
import { Accessor, Setter } from "./2024-11-02/types/signals";
import { Accessor, Setter } from "./v0.2.4-treeshaked/types/signals";
export type Signal<T> = Accessor<T> & { set: Setter<T>; reset: VoidFunction };
export type Signals = Awaited<typeof import("./wrapper.js").default>;
@@ -0,0 +1,700 @@
// @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.w) queueMicrotask(flushSync);
}
var Queue = class {
w = false;
l = [[], [], []];
u = [];
created = clock;
enqueue(type, node) {
this.l[0].push(node);
if (type) this.l[type].push(node);
schedule();
}
run(type) {
if (this.l[type].length) {
if (type === EFFECT_PURE) {
runPureQueue(this.l[type]);
this.l[type] = [];
} else {
const effects = this.l[type];
this.l[type] = [];
runEffectQueue(effects);
}
}
let rerun = false;
for (let i = 0; i < this.u.length; i++) {
rerun = this.u[i].run(type) || rerun;
}
if (type === EFFECT_PURE && this.l[type].length) return true;
}
flush() {
if (this.w) return;
this.w = true;
try {
while (this.run(EFFECT_PURE)) {}
incrementClock();
scheduled = false;
this.run(EFFECT_RENDER);
this.run(EFFECT_USER);
} finally {
this.w = false;
}
}
addChild(child) {
this.u.push(child);
}
removeChild(child) {
const index = this.u.indexOf(child);
if (index >= 0) this.u.splice(index, 1);
}
};
var globalQueue = new Queue();
function flushSync() {
while (scheduled) {
globalQueue.flush();
}
}
function runTop(node) {
const ancestors = [];
for (let current = node; current !== null; current = current.m) {
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].p();
}
}
function runPureQueue(queue) {
for (let i = 0; i < queue.length; i++) {
if (queue[i].a !== STATE_CLEAN) runTop(queue[i]);
}
}
function runEffectQueue(queue) {
for (let i = 0; i < queue.length; i++) queue[i].K();
}
// 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
m = null;
h = null;
n = null;
a = STATE_CLEAN;
g = null;
i = defaultContext;
j = null;
f = globalQueue;
constructor(signal = false) {
if (currentOwner && !signal) currentOwner.append(this);
}
append(child) {
child.m = this;
child.n = this;
if (this.h) this.h.n = child;
child.h = this.h;
this.h = child;
if (child.i !== this.i) {
child.i = { ...this.i, ...child.i };
}
if (this.j) {
child.j = !child.j ? this.j : [...child.j, ...this.j];
}
if (this.f) child.f = this.f;
}
dispose(self = true) {
if (this.a === STATE_DISPOSED) return;
let head = self ? this.n || this.m : this,
current = this.h,
next = null;
while (current && current.m === this) {
current.dispose(true);
current.q();
next = current.h;
current.h = null;
current = next;
}
if (self) this.q();
if (current) current.n = !self ? this : this.n;
if (head) head.h = current;
}
q() {
if (this.n) this.n.h = null;
this.m = null;
this.n = null;
this.i = defaultContext;
this.j = null;
this.a = STATE_DISPOSED;
this.emptyDisposal();
}
emptyDisposal() {
if (!this.g) return;
if (Array.isArray(this.g)) {
for (let i = 0; i < this.g.length; i++) {
const callable = this.g[i];
callable.call(callable);
}
} else {
this.g.call(this.g);
}
this.g = null;
}
handleError(error) {
if (!this.j) throw error;
let i = 0,
len = this.j.length;
for (i = 0; i < len; i++) {
try {
this.j[i](error, this);
break;
} catch (e) {
error = e;
}
}
if (i === len) throw error;
}
};
function onCleanup(fn) {
if (!currentOwner) return fn;
const node = currentOwner;
if (!node.g) {
node.g = fn;
} else if (Array.isArray(node.g)) {
node.g.push(fn);
} else {
node.g = [node.g, fn];
}
return fn;
}
// 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;
B;
r;
// Used in __DEV__ mode, hopefully removed in production
O;
// Using false is an optimization as an alternative to _equals: () => false
// which could enable more efficient DIRTY notification
C = isEqual;
L;
/** 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. */
D = DEFAULT_FLAGS;
E = null;
s = -1;
x = false;
constructor(initialValue, compute2, options) {
super(compute2 === null);
this.r = 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.C = options.equals;
if (options?.unobserved) this.L = options?.unobserved;
}
M() {
if (this.r) {
if (this.d & ERROR_BIT && this.s <= getClock()) update(this);
else this.p();
}
if (!this.r || this.b?.length) track(this);
newFlags |= this.d & ~currentMask;
if (this.d & ERROR_BIT) {
throw this.B;
} else {
return this.e;
}
}
/**
* Return the current value of this computation
* Automatically re-executes the surrounding computation when the value changes
*/
read() {
return this.M();
}
/**
* 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.r && this.d & ERROR_BIT && this.s <= getClock()) {
update(this);
}
if ((notStale || this.d & UNINITIALIZED_BIT) && this.loading()) {
throw new NotReadyError();
}
return this.M();
}
/**
* Return true if the computation is the value is dependent on an unresolved promise
* Triggers re-execution of the computation when the loading state changes
*
* This is useful especially when effects want to re-execute when a computation's
* loading state changes
*/
loading() {
if (this.E === null) {
this.E = loadingState(this);
}
return this.E.read();
}
/** 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.C === false ||
!this.C(this.e, newValue));
if (valueChanged) {
this.e = newValue;
this.B = void 0;
}
const changedFlagsMask = this.d ^ flags,
changedFlags = changedFlagsMask & flags;
this.d = flags;
this.s = 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].N(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.x) return;
this.x = !!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.
*/
N(mask, newFlags2) {
if (this.a >= STATE_DIRTY) return;
if (mask & this.D) {
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].N(mask, newFlags2);
}
}
}
}
F(error) {
this.B = 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
*/
p() {
if (this.a === STATE_DISPOSED) {
throw new Error("Tried to read a disposed computation");
}
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].p();
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
*/
q() {
if (this.a === STATE_DISPOSED) return;
if (this.b) removeSourceObservers(this, 0);
super.q();
}
};
function loadingState(node) {
const prevOwner = setOwner(node.m);
const options = void 0;
const computation = new Computation(
void 0,
() => {
track(node);
node.p();
return !!(node.d & LOADING_BIT);
},
options,
);
computation.D = ERROR_BIT | LOADING_BIT;
setOwner(prevOwner);
return computation;
}
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.r, node);
node.write(result, newFlags, true);
} catch (error) {
if (error instanceof NotReadyError) {
node.write(
UNCHANGED,
newFlags | LOADING_BIT | (node.d & UNINITIALIZED_BIT),
);
} else {
node.F(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.s = 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.L?.();
}
}
}
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?.D ?? 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 {
y;
z;
t;
G = false;
A;
o;
constructor(initialValue, compute2, effect, error, options) {
super(initialValue, compute2, options);
this.y = effect;
this.z = error;
this.A = initialValue;
this.o = options?.render ? EFFECT_RENDER : EFFECT_USER;
if (this.o === EFFECT_RENDER) {
this.r = (p) =>
getClock() > this.f.created && !(this.d & ERROR_BIT)
? latest(() => compute2(p))
: compute2(p);
}
this.p();
!options?.defer &&
(this.o === EFFECT_USER ? this.f.enqueue(this.o, this) : this.K());
}
write(value, flags = 0) {
if (this.a == STATE_DIRTY) {
const currentFlags = this.d;
this.d = flags;
if (
this.o === EFFECT_RENDER &&
(flags & LOADING_BIT) !== (currentFlags & LOADING_BIT)
) {
this.f.H?.(this);
}
}
if (value === UNCHANGED) return this.e;
this.e = value;
this.G = true;
return value;
}
k(state, skipQueue) {
if (this.a >= state || skipQueue) return;
if (this.a === STATE_CLEAN) this.f.enqueue(this.o, this);
this.a = state;
}
F(error) {
this.t?.();
if (this.d & LOADING_BIT) {
this.f.H?.(this);
}
this.d = ERROR_BIT;
if (this.o === EFFECT_USER) {
try {
return this.z
? (this.t = this.z(error))
: console.error(new EffectError(this.y, error));
} catch (e) {
error = e;
}
}
this.handleError(error);
}
q() {
if (this.a === STATE_DISPOSED) return;
this.y = void 0;
this.A = void 0;
this.z = void 0;
this.t?.();
this.t = void 0;
super.q();
}
K() {
if (this.G && this.a !== STATE_DISPOSED) {
this.t?.();
try {
this.t = this.y(this.e, this.A);
} catch (e) {
this.handleError(e);
} finally {
this.A = this.e;
this.G = false;
}
}
}
};
// 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 node = new Computation(first, null, 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) {
resolvedValue = node.wait();
if (!node.b?.length && node.h?.m !== node) {
node.dispose();
node = void 0;
} else if (!node.m && !node.c?.length) {
node.dispose();
node.a = STATE_DIRTY;
}
}
return resolvedValue;
};
}
function createEffect(compute2, effect, error, value, options) {
void new Effect(value, compute2, effect, error, options);
}
function createRoot(init) {
const owner = new Owner();
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,
flushSync,
getOwner,
onCleanup,
runWithOwner,
untrack,
};
@@ -8,3 +8,7 @@ export declare const STATE_CLEAN = 0;
export declare const STATE_CHECK = 1;
export declare const STATE_DIRTY = 2;
export declare const STATE_DISPOSED = 3;
export declare const EFFECT_PURE = 0;
export declare const EFFECT_RENDER = 1;
export declare const EFFECT_USER = 2;
export declare const SUPPORTS_PROXY: boolean;
@@ -28,6 +28,7 @@
*/
import { type Flags } from "./flags.js";
import { Owner } from "./owner.js";
import { type IQueue } from "./scheduler.js";
export interface SignalOptions<T> {
name?: string;
equals?: ((prev: T, next: T) => boolean) | false;
@@ -42,7 +43,7 @@ interface SourceType {
}
interface ObserverType {
_sources: SourceType[] | null;
_notify: (state: number) => void;
_notify: (state: number, skipQueue?: boolean) => void;
_handlerMask: Flags;
_notifyFlags: (mask: Flags, newFlags: Flags) => void;
_time: number;
@@ -50,14 +51,14 @@ interface ObserverType {
/**
* Returns the current observer.
*/
export declare function getObserver(): ObserverType | null;
export declare function incrementClock(): void;
export declare function getObserver(): Computation | null;
export declare const UNCHANGED: unique symbol;
export type UNCHANGED = typeof UNCHANGED;
export declare class Computation<T = any> extends Owner implements SourceType, ObserverType {
_sources: SourceType[] | null;
_observers: ObserverType[] | null;
_value: T | undefined;
_error: unknown;
_compute: null | ((p?: T) => T);
_name: string | undefined;
_equals: false | ((a: T, b: T) => boolean);
@@ -66,9 +67,9 @@ export declare class Computation<T = any> extends Owner implements SourceType, O
_stateFlags: number;
/** Which flags raised by sources are handled, vs. being passed through. */
_handlerMask: number;
_error: Computation<boolean> | null;
_loading: Computation<boolean> | null;
_time: number;
_forceNotify: boolean;
constructor(initialValue: T | undefined, compute: null | ((p?: T) => T), options?: SignalOptions<T>);
_read(): T;
/**
@@ -92,17 +93,12 @@ export declare class Computation<T = any> extends Owner implements SourceType, O
* loading state changes
*/
loading(): boolean;
/**
* Return true if the computation is the computation threw an error
* Triggers re-execution of the computation when the error state changes
*/
error(): boolean;
/** Update the computation with a new value. */
write(value: T | ((currentValue: T) => T) | UNCHANGED, flags?: number, raw?: boolean): T;
/**
* Set the current node's state, and recursively mark all of this node's observers as STATE_CHECK
*/
_notify(state: number): void;
_notify(state: number, skipQueue?: boolean): void;
/**
* Notify the computation that one of its sources has changed flags.
*
@@ -144,18 +140,31 @@ export declare function untrack<T>(fn: () => T): T;
*/
export declare function hasUpdated(fn: () => any): boolean;
/**
* Returns true if the given function contains async signals that are not ready yet.
* Returns true if the given function contains async signals are out of date.
*/
export declare function isPending(fn: () => any): boolean;
export declare function latest<T>(fn: () => T): T | undefined;
export declare function isPending(fn: () => any, loadingValue: boolean): boolean;
/**
* Attempts to resolve value of expression synchronously returning the last resolved value for any async computation.
*/
export declare function latest<T>(fn: () => T): T;
export declare function latest<T, U>(fn: () => T, fallback: U): T | U;
export declare function catchError(fn: () => void): unknown | undefined;
/**
* Runs the given function in the given observer.
*
* Warning: Usually there are simpler ways of modeling a problem that avoid using this function
*/
export declare function runWithObserver<T>(observer: Computation, run: () => T): T | undefined;
/**
* A convenient wrapper that calls `compute` with the `owner` and `observer` and is guaranteed
* to reset the global context after the computation is finished even if an error is thrown.
*/
export declare function compute<T>(owner: Owner | null, compute: (val: T) => T, observer: Computation<T>): T;
export declare function compute<T>(owner: Owner | null, compute: (val: undefined) => T, observer: null): T;
export declare class EagerComputation<T = any> extends Computation<T> {
constructor(initialValue: T, compute: () => T, options?: SignalOptions<T>);
_notify(state: number): void;
}
export declare function compute<T>(owner: Owner | null, fn: (val: T) => T, observer: Computation<T>): T;
export declare function compute<T>(owner: Owner | null, fn: (val: undefined) => T, observer: null): T;
export declare function flatten(children: any, options?: {
skipNonRendered?: boolean;
doNotUnwrap?: boolean;
}): any;
export declare function createBoundary<T>(fn: () => T, queue: IQueue): T;
export {};
@@ -0,0 +1,34 @@
import { EFFECT_RENDER, EFFECT_USER } from "./constants.js";
import { Computation, type SignalOptions } from "./core.js";
/**
* Effects are the leaf nodes of our reactive graph. When their sources change, they are
* automatically added to the queue of effects to re-execute, which will cause them to fetch their
* sources and recompute
*/
export declare class Effect<T = any> extends Computation<T> {
_effect: (val: T, prev: T | undefined) => void | (() => void);
_onerror: ((err: unknown) => void | (() => void)) | undefined;
_cleanup: (() => void) | undefined;
_modified: boolean;
_prevValue: T | undefined;
_type: typeof EFFECT_RENDER | typeof EFFECT_USER;
constructor(initialValue: T, compute: (val?: T) => T, effect: (val: T, prev: T | undefined) => void | (() => void), error?: (err: unknown) => void | (() => void), options?: SignalOptions<T> & {
render?: boolean;
defer?: boolean;
});
write(value: T, flags?: number): T;
_notify(state: number, skipQueue?: boolean): void;
_setError(error: unknown): void;
_disposeNode(): void;
_runEffect(): void;
}
export declare class EagerComputation<T = any> extends Computation<T> {
constructor(initialValue: T, compute: () => T, options?: SignalOptions<T> & {
defer?: boolean;
});
_notify(state: number, skipQueue?: boolean): void;
}
export declare class ProjectionComputation extends Computation {
constructor(compute: () => void);
_notify(state: number, skipQueue?: boolean): void;
}
@@ -1,3 +1,4 @@
import type { Owner } from "./owner.js";
export declare class NotReadyError extends Error {
}
export declare class NoOwnerError extends Error {
@@ -6,6 +7,9 @@ export declare class NoOwnerError extends Error {
export declare class ContextNotFoundError extends Error {
constructor();
}
export interface ErrorHandler {
(error: unknown): void;
export declare class EffectError extends Error {
constructor(effect: Function, cause: unknown);
}
export interface ErrorHandler {
(error: unknown, node: Owner): void;
}
@@ -5,4 +5,7 @@ export declare const ERROR: unique symbol;
export declare const LOADING_OFFSET = 1;
export declare const LOADING_BIT: number;
export declare const LOADING: unique symbol;
export declare const UNINITIALIZED_OFFSET = 2;
export declare const UNINITIALIZED_BIT: number;
export declare const UNINITIALIZED: unique symbol;
export declare const DEFAULT_FLAGS: number;
@@ -0,0 +1,8 @@
export { ContextNotFoundError, NoOwnerError, NotReadyError, type ErrorHandler } from "./error.js";
export { Owner, createContext, getContext, setContext, hasContext, getOwner, onCleanup, type Context, type ContextRecord, type Disposable } from "./owner.js";
export { Computation, createBoundary, getObserver, isEqual, untrack, hasUpdated, isPending, latest, flatten, catchError, UNCHANGED, compute, runWithObserver, type SignalOptions } from "./core.js";
export { Effect, EagerComputation } from "./effect.js";
export { flushSync, getClock, incrementClock, type IQueue, Queue } from "./scheduler.js";
export { createSuspense } from "./suspense.js";
export { SUPPORTS_PROXY } from "./constants.js";
export * from "./flags.js";
@@ -28,6 +28,7 @@
* Note that the owner tree is largely orthogonal to the reactivity tree, and is much closer to the component tree.
*/
import { type ErrorHandler } from "./error.js";
import { type IQueue } from "./scheduler.js";
export type ContextRecord = Record<string | symbol, unknown>;
export interface Disposable {
(): void;
@@ -45,6 +46,7 @@ export declare class Owner {
_disposal: Disposable | Disposable[] | null;
_context: ContextRecord;
_handlers: ErrorHandler[] | null;
_queue: IQueue;
constructor(signal?: boolean);
append(child: Owner): void;
dispose(this: Owner, self?: boolean): void;
@@ -83,6 +85,11 @@ export declare function setContext<T>(context: Context<T>, value?: T, owner?: Ow
*/
export declare function hasContext(context: Context<any>, owner?: Owner | null): boolean;
/**
* Runs the given function when the parent owner computation is being disposed.
* Runs an effect once before the reactive scope is disposed
* @param fn an effect that should run only once on cleanup
*
* @returns the same {@link fn} function that was passed in
*
* @description https://docs.solidjs.com/reference/lifecycle/on-cleanup
*/
export declare function onCleanup(disposable: Disposable): void;
export declare function onCleanup(fn: Disposable): Disposable;
@@ -0,0 +1,29 @@
import type { Computation } from "./core.js";
import type { Effect } from "./effect.js";
export declare function getClock(): number;
export declare function incrementClock(): void;
export interface IQueue {
enqueue<T extends Computation | Effect>(type: number, node: T): void;
run(type: number): boolean | void;
flush(): void;
addChild(child: IQueue): void;
removeChild(child: IQueue): void;
created: number;
}
export declare class Queue implements IQueue {
_running: boolean;
_queues: [Computation[], Effect[], Effect[]];
_children: IQueue[];
created: number;
enqueue<T extends Computation | Effect>(type: number, node: T): void;
run(type: number): true | undefined;
flush(): void;
addChild(child: IQueue): void;
removeChild(child: IQueue): void;
}
export declare const globalQueue: Queue;
/**
* By default, changes are batched on the microtask queue which is an async process. You can flush
* the queue synchronously to get the latest updates by calling `flushSync()`.
*/
export declare function flushSync(): void;
@@ -0,0 +1,11 @@
import { Computation } from "./core.js";
import { type Effect } from "./effect.js";
import { Queue } from "./scheduler.js";
export declare class SuspenseQueue extends Queue {
_nodes: Set<Effect>;
_fallback: boolean;
_signal: Computation<boolean>;
run(type: number): true | undefined;
_update(node: Effect): void;
}
export declare function createSuspense(fn: () => any, fallback: () => any): () => any;
@@ -0,0 +1,2 @@
export { Owner, flushSync, getOwner, onCleanup, untrack } from "./core/index.js";
export * from "./signals.js";
@@ -0,0 +1,22 @@
import type { Accessor } from "./signals.js";
export type Maybe<T> = T | void | null | undefined | false;
/**
* Reactively transforms an array with a callback function - underlying helper for the `<For>` control flow
*
* similar to `Array.prototype.map`, but gets the value and index as accessors, transforms only values that changed and returns an accessor and reactively tracks changes to the list.
*
* @description https://docs.solidjs.com/reference/reactive-utilities/map-array
*/
export declare function mapArray<Item, MappedItem>(list: Accessor<Maybe<readonly Item[]>>, map: (value: Accessor<Item>, index: Accessor<number>) => MappedItem, options?: {
keyed?: boolean | ((item: Item) => any);
fallback?: Accessor<any>;
}): Accessor<MappedItem[]>;
/**
* Reactively repeats a callback function the count provided - underlying helper for the `<Repeat>` control flow
*
* @description https://docs.solidjs.com/reference/reactive-utilities/repeat
*/
export declare function repeat(count: Accessor<number>, map: (index: number) => any, options?: {
from?: Accessor<number | undefined>;
fallback?: Accessor<any>;
}): Accessor<any[]>;
@@ -0,0 +1,105 @@
import type { SignalOptions } from "./core/index.js";
import { Owner } from "./core/index.js";
export type Accessor<T> = () => T;
export type Setter<in out T> = {
<U extends T>(...args: undefined extends T ? [] : [value: Exclude<U, Function> | ((prev: T) => U)]): undefined extends T ? undefined : U;
<U extends T>(value: (prev: T) => U): U;
<U extends T>(value: Exclude<U, Function>): U;
<U extends T>(value: Exclude<U, Function> | ((prev: T) => U)): U;
};
export type Signal<T> = [get: Accessor<T>, set: Setter<T>];
export type ComputeFunction<Prev, Next extends Prev = Prev> = (v: Prev) => Next;
export type EffectFunction<Prev, Next extends Prev = Prev> = (v: Next, p?: Prev) => (() => void) | void;
export interface EffectOptions {
name?: string;
defer?: boolean;
}
export interface MemoOptions<T> {
name?: string;
equals?: false | ((prev: T, next: T) => boolean);
}
export type NoInfer<T extends any> = [T][T extends any ? 0 : never];
/**
* Creates a simple reactive state with a getter and setter
* ```typescript
* const [state: Accessor<T>, setState: Setter<T>] = createSignal<T>(
* value: T,
* options?: { name?: string, equals?: false | ((prev: T, next: T) => boolean) }
* )
* ```
* @param value initial value of the state; if empty, the state's type will automatically extended with undefined; otherwise you need to extend the type manually if you want setting to undefined not be an error
* @param options optional object with a name for debugging purposes and equals, a comparator function for the previous and next value to allow fine-grained control over the reactivity
*
* @returns ```typescript
* [state: Accessor<T>, setState: Setter<T>]
* ```
* * the Accessor is a function that returns the current value and registers each call to the reactive root
* * the Setter is a function that allows directly setting or mutating the value:
* ```typescript
* const [count, setCount] = createSignal(0);
* setCount(count => count + 1);
* ```
*
* @description https://docs.solidjs.com/reference/basic-reactivity/create-signal
*/
export declare function createSignal<T>(): Signal<T | undefined>;
export declare function createSignal<T>(value: Exclude<T, Function>, options?: SignalOptions<T>): Signal<T>;
export declare function createSignal<T>(fn: ComputeFunction<T>, initialValue?: T, options?: SignalOptions<T>): Signal<T>;
/**
* Creates a readonly derived reactive memoized signal
* ```typescript
* export function createMemo<T>(
* compute: (v: T) => T,
* value?: T,
* options?: { name?: string, equals?: false | ((prev: T, next: T) => boolean) }
* ): () => T;
* ```
* @param compute a function that receives its previous or the initial value, if set, and returns a new value used to react on a computation
* @param value an optional initial value for the computation; if set, fn will never receive undefined as first argument
* @param options allows to set a name in dev mode for debugging purposes and use a custom comparison function in equals
*
* @description https://docs.solidjs.com/reference/basic-reactivity/create-memo
*/
export declare function createMemo<Next extends Prev, Prev = Next>(compute: ComputeFunction<undefined | NoInfer<Prev>, Next>): Accessor<Next>;
export declare function createMemo<Next extends Prev, Init = Next, Prev = Next>(compute: ComputeFunction<Init | Prev, Next>, value: Init, options?: MemoOptions<Next>): Accessor<Next>;
/**
* Creates a reactive effect that runs after the render phase
* ```typescript
* export function createEffect<T>(
* compute: (prev: T) => T,
* effect: (v: T, prev: T) => (() => void) | void,
* value?: T,
* options?: { name?: string }
* ): void;
* ```
* @param compute a function that receives its previous or the initial value, if set, and returns a new value used to react on a computation
* @param effect a function that receives the new value and is used to perform side effects, return a cleanup function to run on disposal
* @param error an optional function that receives an error if thrown during the computation
* @param value an optional initial value for the computation; if set, fn will never receive undefined as first argument
* @param options allows to set a name in dev mode for debugging purposes
*
* @description https://docs.solidjs.com/reference/basic-reactivity/create-effect
*/
export declare function createEffect<Next>(compute: ComputeFunction<undefined | NoInfer<Next>, Next>, effect: EffectFunction<NoInfer<Next>, Next>, error?: (err: unknown) => void): void;
export declare function createEffect<Next, Init = Next>(compute: ComputeFunction<Init | Next, Next>, effect: EffectFunction<Next, Next>, error: ((err: unknown) => void) | undefined, value: Init, options?: EffectOptions): void;
/**
* Creates a new non-tracked reactive context with manual disposal
*
* @param fn a function in which the reactive state is scoped
* @returns the output of `fn`.
*
* @description https://docs.solidjs.com/reference/reactive-utilities/create-root
*/
export declare function createRoot<T>(init: ((dispose: () => void) => T) | (() => T)): T;
/**
* Runs the given function in the given owner to move ownership of nested primitives and cleanups.
* This method untracks the current scope.
*
* Warning: Usually there are simpler ways of modeling a problem that avoid using this function
*/
export declare function runWithOwner<T>(owner: Owner | null, run: () => T): T;
/**
* Returns a promise of the resolved value of a reactive expression
* @param fn a reactive expression to resolve
*/
export declare function resolve<T>(fn: () => T): Promise<T>;
@@ -0,0 +1,6 @@
export type { Store, StoreSetter, StoreNode, NotWrappable, SolidStore } from "./store.js";
export type { Merge, Omit } from "./utils.js";
export { unwrap, isWrappable, createStore, $RAW, $TRACK, $PROXY, $TARGET } from "./store.js";
export { createProjection } from "./projection.js";
export { reconcile } from "./reconcile.js";
export { merge, omit } from "./utils.js";
@@ -0,0 +1,8 @@
import { type Store, type StoreSetter } from "./store.js";
/**
* Creates a mutable derived value
*
* @see {@link https://github.com/solidjs/x-reactivity#createprojection}
*/
export declare function createProjection<T extends Object>(fn: (draft: T) => void, initialValue?: T): Store<T>;
export declare function wrapProjection<T>(fn: (draft: T) => void, store: Store<T>, setStore: StoreSetter<T>): [Store<T>, StoreSetter<T>];
@@ -0,0 +1 @@
export declare function reconcile<T extends U, U>(value: T, key: string | ((item: NonNullable<any>) => any)): (state: U) => T;
@@ -1,13 +1,22 @@
import { Computation } from "../core/index.js";
export type Store<T> = Readonly<T>;
export type StoreSetter<T> = (fn: (state: T) => void) => void;
declare const $TRACK: unique symbol, $PROXY: unique symbol;
export { $PROXY, $TRACK };
export type StoreNode = Record<PropertyKey, any>;
type DataNode = Computation<any>;
type DataNodes = Record<PropertyKey, DataNode>;
declare const $RAW: unique symbol, $TRACK: unique symbol, $TARGET: unique symbol, $PROXY: unique symbol;
export declare const STORE_VALUE = "v", STORE_NODE = "n", STORE_HAS = "h";
export { $PROXY, $TRACK, $RAW, $TARGET };
export type StoreNode = {
[STORE_VALUE]: Record<PropertyKey, any>;
[STORE_NODE]?: DataNodes;
[STORE_HAS]?: DataNodes;
};
export declare namespace SolidStore {
interface Unwrappable {
}
}
export type NotWrappable = string | number | bigint | symbol | boolean | Function | null | undefined | SolidStore.Unwrappable[keyof SolidStore.Unwrappable];
export declare function wrap<T extends Record<PropertyKey, any>>(value: T): T;
export declare function isWrappable<T>(obj: T | NotWrappable): obj is T;
/**
* Returns the underlying data in the store without a proxy.
@@ -20,12 +29,6 @@ export declare function isWrappable<T>(obj: T | NotWrappable): obj is T;
* initial === unwrap(state); // => true
* ```
*/
export declare function unwrap<T>(item: T, set?: Set<unknown>): T;
export declare function unwrap<T>(item: T, deep?: boolean, set?: Set<unknown>): T;
export declare function createStore<T extends object = {}>(store: T | Store<T>): [get: Store<T>, set: StoreSetter<T>];
export declare function createStore<T extends object = {}>(fn: (store: T) => void, store: T | Store<T>): [get: Store<T>, set: StoreSetter<T>];
/**
* Creates a mutable derived value
*
* @see {@link https://github.com/solidjs/x-reactivity#createprojection}
*/
export declare function createProjection<T extends Object>(fn: (draft: T) => void, initialValue: T): Readonly<T>;
@@ -0,0 +1,29 @@
type DistributeOverride<T, F> = T extends undefined ? F : T;
type Override<T, U> = T extends any ? U extends any ? {
[K in keyof T]: K extends keyof U ? DistributeOverride<U[K], T[K]> : T[K];
} & {
[K in keyof U]: K extends keyof T ? DistributeOverride<U[K], T[K]> : U[K];
} : T & U : T & U;
type OverrideSpread<T, U> = T extends any ? {
[K in keyof ({
[K in keyof T]: any;
} & {
[K in keyof U]?: any;
} & {
[K in U extends any ? keyof U : keyof U]?: any;
})]: K extends keyof T ? Exclude<U extends any ? U[K & keyof U] : never, undefined> | T[K] : U extends any ? U[K & keyof U] : never;
} : T & U;
type Simplify<T> = T extends any ? {
[K in keyof T]: T[K];
} : T;
type _Merge<T extends unknown[], Curr = {}> = T extends [
infer Next | (() => infer Next),
...infer Rest
] ? _Merge<Rest, Override<Curr, Next>> : T extends [...infer Rest, infer Next | (() => infer Next)] ? Override<_Merge<Rest, Curr>, Next> : T extends [] ? Curr : T extends (infer I | (() => infer I))[] ? OverrideSpread<Curr, I> : Curr;
export type Merge<T extends unknown[]> = Simplify<_Merge<T>>;
export declare function merge<T extends unknown[]>(...sources: T): Merge<T>;
export type Omit<T, K extends readonly (keyof T)[]> = {
[P in keyof T as Exclude<P, K[number]>]: T[P];
};
export declare function omit<T extends Record<any, any>, K extends readonly (keyof T)[]>(props: T, ...keys: K): Omit<T, K>;
export {};
@@ -1,78 +1,99 @@
// @ts-check
/**
* @import { SignalOptions } from "./2024-11-02/types/core/core"
* @import { getOwner as GetOwner, onCleanup as OnCleanup, Owner } from "./2024-11-02/types/core/owner"
* @import { createSignal as CreateSignal, createEffect as CreateEffect, Accessor, Setter, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner } from "./2024-11-02/types/signals";
* @import { SignalOptions } from "./v0.2.4-treeshaked/types/core/core"
* @import { getOwner as GetOwner, onCleanup as OnCleanup, Owner } from "./v0.2.4-treeshaked/types/core/owner"
* @import { createSignal as CreateSignal, createEffect as CreateEffect, Accessor, Setter, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner } from "./v0.2.4-treeshaked/types/signals";
* @import { Signal } from "./types";
*/
const importSignals = import("./2024-11-02/script.js").then((_signals) => {
const signals = {
createSolidSignal: /** @type {CreateSignal} */ (_signals.createSignal),
createSolidEffect: /** @type {CreateEffect} */ (_signals.createEffect),
createEffect: /** @type {CreateEffect} */ (compute, effect) => {
let dispose = /** @type {VoidFunction | null} */ (null);
// @ts-ignore
_signals.createEffect(compute, (v) => {
dispose?.();
signals.createRoot((_dispose) => {
dispose = _dispose;
effect(v);
});
signals.onCleanup(() => dispose?.());
});
signals.onCleanup(() => dispose?.());
},
createMemo: /** @type {CreateMemo} */ (_signals.createMemo),
createRoot: /** @type {CreateRoot} */ (_signals.createRoot),
getOwner: /** @type {GetOwner} */ (_signals.getOwner),
runWithOwner: /** @type {RunWithOwner} */ (_signals.runWithOwner),
onCleanup: /** @type {OnCleanup} */ (_signals.onCleanup),
flushSync: _signals.flushSync,
/**
* @template T
* @param {T} initialValue
* @param {SignalOptions<T> & {save?: {keyPrefix: 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
get.reset = () => set(initialValue);
if (options?.save) {
const save = options.save;
const paramKey = save.key;
const storageKey = `${save.keyPrefix}-${paramKey}`;
let serialized = /** @type {string | null} */ (null);
if (options.save.serializeParam !== false) {
serialized = new URLSearchParams(window.location.search).get(
paramKey,
);
const importSignals = import("./v0.2.4-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);
// @ts-ignore
_signals.createEffect(compute, (v) => {
dispose?.();
signals.createRoot((_dispose) => {
dispose = _dispose;
effect(v);
});
signals.onCleanup(() => dispose?.());
});
signals.onCleanup(() => dispose?.());
}
),
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),
flushSync: _signals.flushSync,
/**
* @template T
* @param {T} initialValue
* @param {SignalOptions<T> & {save?: {keyPrefix: 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,
);
if (serialized === null) {
serialized = localStorage.getItem(storageKey);
}
if (serialized) {
set(save.deserialize(serialized));
}
// @ts-ignore
get.set = set;
let firstEffect = true;
this.createEffect(get, (value) => {
if (!save) return;
// @ts-ignore
get.reset = () => set(initialValue);
if (options?.save) {
const save = options.save;
const paramKey = save.key;
const storageKey = `${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(() => save.deserialize(serialized));
}
let firstEffect = true;
this.createEffect(get, (value) => {
if (!save) return;
if (!firstEffect) {
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 (!firstEffect) {
if (
value !== undefined &&
value !== null &&
@@ -80,35 +101,23 @@ const importSignals = import("./2024-11-02/script.js").then((_signals) => {
initialValue === null ||
save.serialize(value) !== save.serialize(initialValue))
) {
localStorage.setItem(storageKey, save.serialize(value));
writeParam(paramKey, save.serialize(value));
} else {
localStorage.removeItem(storageKey);
removeParam(paramKey);
}
}
if (
value !== undefined &&
value !== null &&
(initialValue === undefined ||
initialValue === null ||
save.serialize(value) !== save.serialize(initialValue))
) {
writeParam(paramKey, save.serialize(value));
} else {
removeParam(paramKey);
}
firstEffect = false;
});
}
firstEffect = false;
});
}
// @ts-ignore
return get;
},
};
// @ts-ignore
return get;
},
};
return signals;
});
return signals;
},
);
/**
* @param {string} key
+21 -14
View File
@@ -28,14 +28,14 @@ export function init({
elements.charts.append(utils.dom.createShadow("left"));
elements.charts.append(utils.dom.createShadow("right"));
const { headerElement, titleElement } = utils.dom.createHeader({});
const { headerElement, headingElement } = utils.dom.createHeader({});
elements.charts.append(headerElement);
const chart = lightweightCharts.createChartElement({
parent: elements.charts,
signals,
colors,
id: "chart",
id: "charts",
utils,
vecsResources,
});
@@ -45,10 +45,8 @@ export function init({
let firstRun = true;
signals.createEffect(selected, (option) => {
titleElement.innerHTML = option.title;
headingElement.innerHTML = option.title;
signals.createEffect(index_, (index) => {
utils.url.writeParam("index", String(index));
chart.reset({ owner: signals.getOwner() });
const TIMERANGE_LS_KEY = `chart-timerange-${index}`;
@@ -72,7 +70,7 @@ export function init({
chart.create({
index,
timeScaleSetCallback: () => {
timeScaleSetCallback: (unknownTimeScaleCallback) => {
const from_ = from();
const to_ = to();
if (from_ !== null && to_ !== null) {
@@ -80,6 +78,8 @@ export function init({
from: from_,
to: to_,
});
} else {
unknownTimeScaleCallback();
}
},
});
@@ -87,6 +87,7 @@ export function init({
const candles = chart.addCandlestickSeries({
vecId: "ohlc",
name: "Price",
unit: "US Dollars",
});
signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
if (!latest) return;
@@ -98,17 +99,18 @@ export function init({
});
[
{ blueprints: option.top, paneNumber: 0 },
{ blueprints: option.bottom, paneNumber: 1 },
].forEach(({ blueprints, paneNumber }) => {
{ blueprints: option.top, paneIndex: 0 },
{ blueprints: option.bottom, paneIndex: 1 },
].forEach(({ blueprints, paneIndex }) => {
blueprints?.forEach((blueprint) => {
if (vecIdToIndexes[blueprint.key].includes(index)) {
chart.addLineSeries({
vecId: blueprint.key,
color: blueprint.color,
name: blueprint.title,
unit: option.unit,
defaultActive: blueprint.defaultActive,
paneNumber,
paneIndex,
});
}
});
@@ -136,7 +138,6 @@ export function init({
* @param {Utilities} args.utils
*/
function createIndexSelector({ elements, signals, utils }) {
const indexLSKey = "chart-index";
const indexChoices = /**@type {const} */ ([
"timestamp",
"date",
@@ -149,8 +150,14 @@ function createIndexSelector({ elements, signals, utils }) {
"decade",
]);
/** @typedef {(typeof indexChoices)[number]} SerializedIndex */
const serializedIndex = signals.createSignal(
/** @type {SerializedIndex} */ (localStorage.getItem(indexLSKey) || "date"),
const serializedIndex = /** @type {Signal<SerializedIndex>} */ (
signals.createSignal("date", {
save: {
keyPrefix: "charts",
key: "index",
...utils.serde.string,
},
})
);
const indexesField = utils.dom.createHorizontalChoiceField({
title: "Index",
@@ -162,12 +169,12 @@ function createIndexSelector({ elements, signals, utils }) {
indexesField.addEventListener("change", (event) => {
// @ts-ignore
const value = event.target.value;
localStorage.setItem(indexLSKey, value);
serializedIndex.set(value);
});
const fieldset = window.document.createElement("fieldset");
fieldset.append(indexesField);
fieldset.dataset.size = "sm";
elements.charts.append(fieldset);
const index = signals.createMemo(
+53 -42
View File
@@ -1,13 +1,13 @@
// @ts-check
/**
* @import { Option, Weighted, Color, DatasetCandlestickData, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, Valued, SingleValueData, CandlestickData, ChartData, OHLCTuple } from "./types/self"
* @import { Marker, CreatePaneParameters, HoveredLegend, ChartPane, SplitSeries, SingleSeries, CreateSplitSeriesParameters, LineSeriesBlueprint, CandlestickSeriesBlueprint, BaselineSeriesBlueprint, CreateBaseSeriesParameters, BaseSeries, RemoveSeriesBlueprintFluff, SplitSeriesBlueprint, AnySeries, PriceSeriesType } from "../packages/lightweight-charts/types";
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, Unit, AnySeriesBlueprint } from "./options"
* @import {Valued, SingleValueData, CandlestickData, ChartData, OHLCTuple} from "../packages/lightweight-charts/wrapper"
* @import * as _ from "../packages/ufuzzy/v1.0.14/types"
* @import { createChart as CreateClassicChart, createChartEx as CreateCustomChart, LineStyleOptions, DeepPartial, ChartOptions, IChartApi, IHorzScaleBehavior, WhitespaceData, ISeriesApi, Time, LineData, LogicalRange, SeriesMarker, SeriesType, BaselineStyleOptions, SeriesOptionsCommon } from "../packages/lightweight-charts/v5.0.5/types"
* @import { SignalOptions } from "../packages/solid-signals/2024-11-02/types/core/core"
* @import { getOwner as GetOwner, onCleanup as OnCleanup, Owner } from "../packages/solid-signals/2024-11-02/types/core/owner"
* @import { createSignal as CreateSignal, createEffect as CreateEffect, Accessor, Setter, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner } from "../packages/solid-signals/2024-11-02/types/signals";
* @import { createChart as CreateClassicChart, LineStyleOptions, DeepPartial, ChartOptions, IChartApi, IHorzScaleBehavior, WhitespaceData, ISeriesApi, Time, LineData, LogicalRange, SeriesType, BaselineStyleOptions, SeriesOptionsCommon, BaselineData, CandlestickStyleOptions } from "../packages/lightweight-charts/v5.0.5-treeshaked/types"
* @import { SignalOptions } from "../packages/solid-signals/v0.2.4-treeshaked/types/core/core"
* @import { getOwner as GetOwner, onCleanup as OnCleanup, Owner } from "../packages/solid-signals/v0.2.4-treeshaked/types/core/owner"
* @import { createSignal as CreateSignal, createEffect as CreateEffect, Accessor, Setter, createMemo as CreateMemo } from "../packages/solid-signals/v0.2.4-treeshaked/types/signals";
* @import {Signal, Signals} from "../packages/solid-signals/types";
* @import {Addressindex, Dateindex, Decadeindex, Difficultyepoch, Index, Halvingepoch, Height, Monthindex, P2PK33index, P2PK65index, P2PKHindex, P2SHindex, P2TRindex, P2WPKHindex, P2WSHindex, Txindex, Txinindex, Txoutindex, VecId, Weekindex, Yearindex, VecIdToIndexes, Quarterindex} from "./vecid-to-indexes"
*/
@@ -490,30 +490,21 @@ function createUtils() {
return { input, signal };
},
/**
* @param {Object} param0
* @param {string} [param0.title]
* @param {Object} args
* @param {1 | 2 | 3} [args.level]
* @param {string} [args.title]
*/
createHeader({ title }) {
createHeader({ title, level = 1 }) {
const headerElement = window.document.createElement("header");
const div = window.document.createElement("div");
headerElement.append(div);
const h1 = window.document.createElement("h1");
div.append(h1);
h1.style.display = "flex";
h1.style.flexDirection = "column";
const titleElement = window.document.createElement("span");
if (title) {
titleElement.append(title);
}
h1.append(titleElement);
titleElement.style.display = "block";
const headingElement = window.document.createElement(`h${level}`);
headingElement.innerHTML = title || "";
headerElement.append(headingElement);
headingElement.style.display = "block";
return {
headerElement,
titleElement,
headingElement,
};
},
/**
@@ -550,11 +541,11 @@ function createUtils() {
select.append(optGroup);
list.forEach((option) => {
optGroup.append(this.createOption(option));
setters[option.value] = () => signal.set(option);
setters[option.value] = () => signal.set(() => option);
});
} else {
select.append(this.createOption(anyOption));
setters[anyOption.value] = () => signal.set(anyOption);
setters[anyOption.value] = () => signal.set(() => anyOption);
}
if (index !== list.length - 1) {
select.append(window.document.createElement("hr"));
@@ -852,6 +843,20 @@ function createUtils() {
};
const serde = {
string: {
/**
* @param {string} v
*/
serialize(v) {
return v;
},
/**
* @param {string} v
*/
deserialize(v) {
return v;
},
},
number: {
/**
* @param {number} v
@@ -880,12 +885,12 @@ function createUtils() {
return v ? Number(v) : null;
},
},
date: {
optDate: {
/**
* @param {Date} v
* @param {Date | null} date
*/
serialize(v) {
return date.toString(v);
serialize(date) {
return date !== null ? date.toString() : "";
},
/**
* @param {string} v
@@ -947,11 +952,22 @@ function createUtils() {
},
/**
* @param {Date} date
* @returns {string}
*/
toString(date) {
return date.toJSON().split("T")[0];
},
/**
* @param {Date} date
*/
toDateIndex(date) {
if (
date.getUTCFullYear() === 2009 &&
date.getUTCMonth() === 0 &&
date.getUTCDate() === 3
)
return 0;
return this.differenceBetween(date, new Date("2009-01-09"));
},
/**
* @param {Time} time
*/
@@ -983,7 +999,6 @@ function createUtils() {
/**
* @param {Date} date1
* @param {Date} date2
* @returns
*/
differenceBetween(date1, date2) {
return Math.abs(date1.valueOf() - date2.valueOf()) / this.ONE_DAY_IN_MS;
@@ -1376,7 +1391,6 @@ function getElements() {
searchInput: /** @type {HTMLInputElement} */ (
getElementById("search-input")
),
searchSmall: getElementById("search-small"),
searchResults: getElementById("search-results"),
selectors: getElementById("frame-selectors"),
style: getComputedStyle(window.document.documentElement),
@@ -1605,6 +1619,8 @@ function createColors(dark, elements) {
}
/**
* @typedef {ReturnType<typeof createColors>} Colors
* @typedef {Colors["orange"]} Color
* @typedef {keyof Colors} ColorName
*/
/**
@@ -1672,7 +1688,7 @@ function initWebSockets(signals, utils) {
}
/**
* @param {(candle: DatasetCandlestickData) => void} callback
* @param {(candle: CandlestickData) => void} callback
* @param {number} interval
*/
function krakenCandleWebSocketCreator(callback, interval) {
@@ -1702,7 +1718,7 @@ function initWebSockets(signals, utils) {
const dateStr = utils.date.toString(date);
/** @type {DatasetCandlestickData} */
/** @type {CandlestickData} */
const candle = {
index: -1,
time: dateStr,
@@ -1985,7 +2001,7 @@ function main() {
utils,
webSockets,
vecsResources,
vecIdToIndexes: vecIdToIndexes,
vecIdToIndexes,
}),
),
),
@@ -2017,6 +2033,7 @@ function main() {
lightweightCharts,
signals,
utils,
vecsResources,
}),
),
),
@@ -2108,8 +2125,6 @@ function main() {
const haystack = options.list.map((option) => option.title);
const searchSmallOgInnerHTML = elements.searchSmall.innerHTML;
const RESULTS_PER_PAGE = 100;
packages.ufuzzy().then((ufuzzy) => {
@@ -2193,7 +2208,6 @@ function main() {
});
if (!needle) {
elements.searchSmall.innerHTML = searchSmallOgInnerHTML;
elements.searchResults.innerHTML = "";
return;
}
@@ -2253,9 +2267,6 @@ function main() {
);
}
elements.searchSmall.innerHTML = `Found <strong>${
result?.[0]?.length || 0
}</strong> result(s)`;
elements.searchResults.innerHTML = "";
const list = computeResultPage(result, 0);
File diff suppressed because it is too large Load Diff
@@ -20,8 +20,8 @@ self.addEventListener("install", (_event) => {
"/scripts/simulation.js",
"/styles/simulation.css",
"/packages/lean-qr/v2.3.4/script.js",
"/packages/lightweight-charts/v5.0.4/script.js",
"/packages/solid-signals/2024-11-02/script.js",
"/packages/lightweight-charts/v5.0.5-treeshaked/script.js",
"/packages/solid-signals/v0.2.4-treeshaked/script.js",
"/packages/ufuzzy/v1.0.14/script.js",
]);
}),
+341 -328
View File
@@ -7,11 +7,17 @@
* @param {Signals} args.signals
* @param {Utilities} args.utils
* @param {Elements} args.elements
* @param {VecsResources} args.vecsResources
*/
export function init({ colors, elements, lightweightCharts, signals, utils }) {
export function init({
colors,
elements,
lightweightCharts,
signals,
utils,
vecsResources,
}) {
/**
* @import { ColorName } from './types/self';
*
* @typedef {Object} Frequency
* @property {string} name
* @property {string} value
@@ -151,7 +157,7 @@ export function init({ colors, elements, lightweightCharts, signals, utils }) {
initial: {
amount: signals.createSignal(/** @type {number | null} */ (1000), {
save: {
...utils.serde.number,
...utils.serde.optNumber,
keyPrefix,
key: "initial-amount",
},
@@ -160,7 +166,7 @@ export function init({ colors, elements, lightweightCharts, signals, utils }) {
topUp: {
amount: signals.createSignal(/** @type {number | null} */ (150), {
save: {
...utils.serde.number,
...utils.serde.optNumber,
keyPrefix,
key: "top-up-amount",
},
@@ -181,14 +187,14 @@ export function init({ colors, elements, lightweightCharts, signals, utils }) {
investment: {
initial: signals.createSignal(/** @type {number | null} */ (1000), {
save: {
...utils.serde.number,
...utils.serde.optNumber,
keyPrefix,
key: "initial-swap",
},
}),
recurrent: signals.createSignal(/** @type {number | null} */ (5), {
save: {
...utils.serde.number,
...utils.serde.optNumber,
keyPrefix,
key: "recurrent-swap",
},
@@ -210,7 +216,7 @@ export function init({ colors, elements, lightweightCharts, signals, utils }) {
/** @type {Date | null} */ (new Date("2021-04-15")),
{
save: {
...utils.serde.date,
...utils.serde.optDate,
keyPrefix,
key: "interval-start",
},
@@ -218,7 +224,7 @@ export function init({ colors, elements, lightweightCharts, signals, utils }) {
),
end: signals.createSignal(/** @type {Date | null} */ (new Date()), {
save: {
...utils.serde.date,
...utils.serde.optDate,
keyPrefix,
key: "interval-end",
},
@@ -227,7 +233,7 @@ export function init({ colors, elements, lightweightCharts, signals, utils }) {
fees: {
percentage: signals.createSignal(/** @type {number | null} */ (0.25), {
save: {
...utils.serde.number,
...utils.serde.optNumber,
keyPrefix,
key: "percentage",
},
@@ -257,7 +263,8 @@ export function init({ colors, elements, lightweightCharts, signals, utils }) {
* @param {string} param0.text
*/
function createColoredSpan({ color, text }) {
return `<span style="color: ${colors[color]()}; font-weight: var(--font-weight-bold)">${text}</span>`;
return `<span style="color: ${colors[color]()}; font-weight: 500; text-transform: uppercase;
font-size: var(--font-size-sm);">${text}</span>`;
}
parametersElement.append(
@@ -544,25 +551,19 @@ export function init({ colors, elements, lightweightCharts, signals, utils }) {
parent: resultsElement,
signals,
colors,
id: `simulation-0`,
kind: "static",
scale: "date",
id: `result`,
fitContentOnResize: true,
vecsResources,
utils,
config: [
{
unit: "US Dollars",
config: [
blueprints: [
{
title: "Fees Paid",
title: "Bitcoin Value",
type: "Line",
color: colors.rose,
data: totalFeesPaidData,
},
{
title: "Dollars Left",
type: "Line",
color: colors.offDollars,
data: dollarsLeftData,
color: colors.amber,
data: bitcoinValueData,
},
{
title: "Dollars Converted",
@@ -571,10 +572,18 @@ export function init({ colors, elements, lightweightCharts, signals, utils }) {
data: totalInvestedAmountData,
},
{
title: "Bitcoin Value",
title: "Dollars Left",
type: "Line",
color: colors.amber,
data: bitcoinValueData,
color: colors.offDollars,
data: dollarsLeftData,
defaultActive: false,
},
{
title: "Fees Paid",
type: "Line",
color: colors.rose,
data: totalFeesPaidData,
defaultActive: false,
},
],
},
@@ -585,14 +594,14 @@ export function init({ colors, elements, lightweightCharts, signals, utils }) {
parent: resultsElement,
signals,
colors,
id: `simulation-1`,
scale: "date",
kind: "static",
id: `bitcoin`,
fitContentOnResize: true,
vecsResources,
utils,
config: [
{
unit: "US Dollars",
config: [
unit: "Bitcoin",
blueprints: [
{
title: "Bitcoin Stack",
type: "Line",
@@ -608,14 +617,14 @@ export function init({ colors, elements, lightweightCharts, signals, utils }) {
parent: resultsElement,
signals,
colors,
id: `simulation-average-price`,
scale: "date",
kind: "static",
id: `average-price`,
fitContentOnResize: true,
vecsResources,
utils,
config: [
{
unit: "US Dollars",
config: [
blueprints: [
{
title: "Bitcoin Price",
type: "Line",
@@ -637,28 +646,18 @@ export function init({ colors, elements, lightweightCharts, signals, utils }) {
parent: resultsElement,
signals,
colors,
id: `simulation-return-ratio`,
scale: "date",
kind: "static",
vecsResources,
id: `return-ratio`,
fitContentOnResize: true,
utils,
config: [
{
unit: "US Dollars",
config: [
blueprints: [
{
title: "Return Of Investment",
type: "Baseline",
data: resultData,
// TODO: Doesn't work for some reason
// options: {
// baseLineColor: "#888",
// baseLineVisible: true,
// baseLineWidth: 1,
// baseValue: {
// price: 0,
// type: "price",
// },
// },
},
],
},
@@ -670,323 +669,337 @@ export function init({ colors, elements, lightweightCharts, signals, utils }) {
signals,
colors,
id: `simulation-profitability-ratios`,
kind: "static",
scale: "date",
fitContentOnResize: true,
vecsResources,
utils,
owner,
config: [
{
unit: "Percentage",
config: [
{
title: "Unprofitable Days Ratio",
type: "Line",
color: colors.red,
data: unprofitableDaysRatioData,
},
blueprints: [
{
title: "Profitable Days Ratio",
type: "Line",
color: colors.green,
data: profitableDaysRatioData,
},
{
title: "Unprofitable Days Ratio",
type: "Line",
color: colors.red,
data: unprofitableDaysRatioData,
},
],
},
],
});
const closes = datasets.getOrCreate("date", "date-to-close");
closes.fetchRange(2009, new Date().getUTCFullYear()).then(() => {
signals.runWithOwner(owner, () => {
signals.createEffect(
() => ({
initialDollarAmount: settings.dollars.initial.amount() || 0,
topUpAmount: settings.dollars.topUp.amount() || 0,
topUpFrequency: settings.dollars.topUp.frenquency(),
initialSwap: settings.bitcoin.investment.initial() || 0,
recurrentSwap: settings.bitcoin.investment.recurrent() || 0,
swapFrequency: settings.bitcoin.investment.frequency(),
start: settings.interval.start(),
end: settings.interval.end(),
fees: settings.fees.percentage(),
}),
({
initialDollarAmount,
topUpAmount,
topUpFrequency,
initialSwap,
recurrentSwap,
swapFrequency,
start,
end,
fees,
}) => {
if (!start || !end || start > end) return;
vecsResources
.getOrCreate(/** @satisfies {Dateindex} */ (1), "close")
.fetch()
.then((_closes) => {
if (!_closes) return;
const closes = /** @type {number[]} */ (_closes);
const range = utils.date.getRange(start, end);
signals.runWithOwner(owner, () => {
signals.createEffect(
() => ({
initialDollarAmount: settings.dollars.initial.amount() || 0,
topUpAmount: settings.dollars.topUp.amount() || 0,
topUpFrequency: settings.dollars.topUp.frenquency(),
initialSwap: settings.bitcoin.investment.initial() || 0,
recurrentSwap: settings.bitcoin.investment.recurrent() || 0,
swapFrequency: settings.bitcoin.investment.frequency(),
start: settings.interval.start(),
end: settings.interval.end(),
fees: settings.fees.percentage(),
}),
({
initialDollarAmount,
topUpAmount,
topUpFrequency,
initialSwap,
recurrentSwap,
swapFrequency,
start,
end,
fees,
}) => {
if (!start || !end || start > end) return;
totalInvestedAmountData().length = 0;
bitcoinValueData().length = 0;
bitcoinData().length = 0;
resultData().length = 0;
dollarsLeftData().length = 0;
totalValueData().length = 0;
investmentData().length = 0;
bitcoinAddedData().length = 0;
averagePricePaidData().length = 0;
bitcoinPriceData().length = 0;
buyCountData().length = 0;
totalFeesPaidData().length = 0;
daysCountData().length = 0;
profitableDaysRatioData().length = 0;
unprofitableDaysRatioData().length = 0;
const range = utils.date.getRange(start, end);
let bitcoin = 0;
let sats = 0;
let dollars = initialDollarAmount;
let investedAmount = 0;
let postFeesInvestedAmount = 0;
let buyCount = 0;
let averagePricePaid = 0;
let bitcoinValue = 0;
let roi = 0;
let totalValue = 0;
let totalFeesPaid = 0;
let daysCount = range.length;
let profitableDays = 0;
let unprofitableDays = 0;
let profitableDaysRatio = 0;
let unprofitableDaysRatio = 0;
let lastInvestDay = range[0];
let dailyInvestment = 0;
let bitcoinAdded = 0;
let satsAdded = 0;
let lastSatsAdded = 0;
totalInvestedAmountData().length = 0;
bitcoinValueData().length = 0;
bitcoinData().length = 0;
resultData().length = 0;
dollarsLeftData().length = 0;
totalValueData().length = 0;
investmentData().length = 0;
bitcoinAddedData().length = 0;
averagePricePaidData().length = 0;
bitcoinPriceData().length = 0;
buyCountData().length = 0;
totalFeesPaidData().length = 0;
daysCountData().length = 0;
profitableDaysRatioData().length = 0;
unprofitableDaysRatioData().length = 0;
range.forEach((date, index) => {
const year = date.getUTCFullYear();
const time = utils.date.toString(date);
let bitcoin = 0;
let sats = 0;
let dollars = initialDollarAmount;
let investedAmount = 0;
let postFeesInvestedAmount = 0;
let buyCount = 0;
let averagePricePaid = 0;
let bitcoinValue = 0;
let roi = 0;
let totalValue = 0;
let totalFeesPaid = 0;
let daysCount = range.length;
let profitableDays = 0;
let unprofitableDays = 0;
let profitableDaysRatio = 0;
let unprofitableDaysRatio = 0;
let lastInvestDay = range[0];
let dailyInvestment = 0;
let bitcoinAdded = 0;
let satsAdded = 0;
let lastSatsAdded = 0;
if (topUpFrequency.isTriggerDay(date)) {
dollars += topUpAmount;
}
range.forEach((date, index) => {
const year = date.getUTCFullYear();
const time = utils.date.toString(date);
const close = closes.ranges
.at(utils.chunkIdToIndex("date", year))
?.json()?.dataset.map[utils.date.toString(date)];
if (topUpFrequency.isTriggerDay(date)) {
dollars += topUpAmount;
}
if (!close) return;
const close = closes[utils.date.toDateIndex(date)];
dailyInvestment = 0;
/** @param {number} value */
function invest(value) {
value = Math.min(dollars, value);
dailyInvestment += value;
dollars -= value;
buyCount += 1;
lastInvestDay = date;
}
if (!index) {
invest(initialSwap);
}
if (swapFrequency.isTriggerDay(date) && dollars > 0) {
invest(recurrentSwap);
}
if (!close) return;
investedAmount += dailyInvestment;
dailyInvestment = 0;
/** @param {number} value */
function invest(value) {
value = Math.min(dollars, value);
dailyInvestment += value;
dollars -= value;
buyCount += 1;
lastInvestDay = date;
}
if (!index) {
invest(initialSwap);
}
if (swapFrequency.isTriggerDay(date) && dollars > 0) {
invest(recurrentSwap);
}
let dailyInvestmentPostFees =
dailyInvestment * (1 - (fees || 0) / 100);
investedAmount += dailyInvestment;
totalFeesPaid += dailyInvestment - dailyInvestmentPostFees;
let dailyInvestmentPostFees =
dailyInvestment * (1 - (fees || 0) / 100);
bitcoinAdded = dailyInvestmentPostFees / close;
bitcoin += bitcoinAdded;
satsAdded = Math.floor(bitcoinAdded * 100_000_000);
if (satsAdded > 0) {
lastSatsAdded = satsAdded;
}
sats += satsAdded;
totalFeesPaid += dailyInvestment - dailyInvestmentPostFees;
postFeesInvestedAmount += dailyInvestmentPostFees;
bitcoinAdded = dailyInvestmentPostFees / close;
bitcoin += bitcoinAdded;
satsAdded = Math.floor(bitcoinAdded * 100_000_000);
if (satsAdded > 0) {
lastSatsAdded = satsAdded;
}
sats += satsAdded;
bitcoinValue = close * bitcoin;
postFeesInvestedAmount += dailyInvestmentPostFees;
totalValue = dollars + bitcoinValue;
bitcoinValue = close * bitcoin;
averagePricePaid = postFeesInvestedAmount / bitcoin;
totalValue = dollars + bitcoinValue;
roi = (bitcoinValue / postFeesInvestedAmount - 1) * 100;
averagePricePaid = postFeesInvestedAmount / bitcoin;
const daysCount = index + 1;
profitableDaysRatio = profitableDays / daysCount;
unprofitableDaysRatio = unprofitableDays / daysCount;
roi = (bitcoinValue / postFeesInvestedAmount - 1) * 100;
if (roi >= 0) {
profitableDays += 1;
} else {
unprofitableDays += 1;
}
const daysCount = index + 1;
profitableDaysRatio = profitableDays / daysCount;
unprofitableDaysRatio = unprofitableDays / daysCount;
bitcoinPriceData().push({
time,
value: close,
if (roi >= 0) {
profitableDays += 1;
} else {
unprofitableDays += 1;
}
bitcoinPriceData().push({
time,
value: close,
});
bitcoinData().push({
time,
value: bitcoin,
});
totalInvestedAmountData().push({
time,
value: investedAmount,
});
bitcoinValueData().push({
time,
value: bitcoinValue,
});
resultData().push({
time,
value: roi,
});
dollarsLeftData().push({
time,
value: dollars,
});
totalValueData().push({
time,
value: totalValue,
});
investmentData().push({
time,
value: dailyInvestment,
});
bitcoinAddedData().push({
time,
value: bitcoinAdded,
});
averagePricePaidData().push({
time,
value: averagePricePaid,
});
buyCountData().push({
time,
value: buyCount,
});
totalFeesPaidData().push({
time,
value: totalFeesPaid,
});
daysCountData().push({
time,
value: daysCount,
});
profitableDaysRatioData().push({
time,
value: profitableDaysRatio * 100,
});
unprofitableDaysRatioData().push({
time,
value: unprofitableDaysRatio * 100,
});
});
bitcoinData().push({
time,
value: bitcoin,
});
totalInvestedAmountData().push({
time,
value: investedAmount,
});
bitcoinValueData().push({
time,
value: bitcoinValue,
});
resultData().push({
time,
value: roi,
});
dollarsLeftData().push({
time,
value: dollars,
});
totalValueData().push({
time,
value: totalValue,
});
investmentData().push({
time,
value: dailyInvestment,
});
bitcoinAddedData().push({
time,
value: bitcoinAdded,
});
averagePricePaidData().push({
time,
value: averagePricePaid,
});
buyCountData().push({
time,
value: buyCount,
});
totalFeesPaidData().push({
time,
value: totalFeesPaid,
});
daysCountData().push({
time,
value: daysCount,
});
profitableDaysRatioData().push({
time,
value: profitableDaysRatio * 100,
});
unprofitableDaysRatioData().push({
time,
value: unprofitableDaysRatio * 100,
});
});
const f = utils.locale.numberToUSFormat;
/** @param {number} v */
const fd = (v) => utils.formatters.dollars.format(v);
/** @param {number} v */
const fp = (v) => utils.formatters.percentage.format(v);
/**
* @param {ColorName} c
* @param {string} t
*/
const c = (c, t) => createColoredSpan({ color: c, text: t });
const serInvestedAmount = c("dollars", fd(investedAmount));
const serDaysCount = c("sky", f(daysCount));
const serSats = c("orange", f(sats));
const serBitcoin = c("orange", `~${f(bitcoin)}`);
const serBitcoinValue = c("amber", fd(bitcoinValue));
const serAveragePricePaid = c("lightDollars", fd(averagePricePaid));
const serRoi = c("yellow", fp(roi / 100));
const serDollars = c("offDollars", fd(dollars));
const serTotalFeesPaid = c("rose", fd(totalFeesPaid));
p1.innerHTML = `After exchanging ${serInvestedAmount} in the span of ${serDaysCount} days, you would have accumulated ${serSats} Satoshis (${serBitcoin} Bitcoin) worth today ${serBitcoinValue} at an average price of ${serAveragePricePaid} per Bitcoin with a return of investment of ${serRoi}, have ${serDollars} left and paid a total of ${serTotalFeesPaid} in fees.`;
const dayDiff = Math.floor(
utils.date.differenceBetween(new Date(), lastInvestDay),
);
const serDailyInvestment = c("offDollars", fd(dailyInvestment));
const setLastSatsAdded = c("bitcoin", 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`;
const serProfitableDaysRatio = c("green", fp(profitableDaysRatio));
const serUnprofitableDaysRatio = c("red", fp(unprofitableDaysRatio));
p3.innerHTML = `You would've been ${serProfitableDaysRatio} of the time profitable and ${serUnprofitableDaysRatio} of the time unprofitable.`;
signals.createEffect(lastValues, (lastValues) => {
const lowestAnnual4YReturn = 0.2368;
// const lowestAnnual4YReturn = lastValues?.["price-4y-compound-return"] || 0
const serLowestAnnual4YReturn = c(
"cyan",
`${fp(lowestAnnual4YReturn)}`,
);
const lowestAnnual4YReturnPercentage = 1 + lowestAnnual4YReturn;
/**
* @param {number} power
*/
function bitcoinValueReturn(power) {
return (
bitcoinValue * Math.pow(lowestAnnual4YReturnPercentage, power)
);
}
const bitcoinValueAfter4y = bitcoinValueReturn(4);
const serBitcoinValueAfter4y = c("purple", fd(bitcoinValueAfter4y));
const bitcoinValueAfter10y = bitcoinValueReturn(10);
const serBitcoinValueAfter10y = c(
"fuchsia",
fd(bitcoinValueAfter10y),
);
const bitcoinValueAfter21y = bitcoinValueReturn(21);
const serBitcoinValueAfter21y = c("pink", fd(bitcoinValueAfter21y));
const f = utils.locale.numberToUSFormat;
/** @param {number} v */
p4.innerHTML = `The lowest annual return after 4 years has historically been ${serLowestAnnual4YReturn}.<br/>Using it as the baseline, your Bitcoin would be worth ${serBitcoinValueAfter4y} after 4 years, ${serBitcoinValueAfter10y} after 10 years and ${serBitcoinValueAfter21y} after 21 years.`;
});
const fd = (v) => utils.formatters.dollars.format(v);
/** @param {number} v */
const fp = (v) => utils.formatters.percentage.format(v);
/**
* @param {ColorName} c
* @param {string} t
*/
const c = (c, t) => createColoredSpan({ color: c, text: t });
totalInvestedAmountData.set((a) => a);
bitcoinValueData.set((a) => a);
bitcoinData.set((a) => a);
resultData.set((a) => a);
dollarsLeftData.set((a) => a);
totalValueData.set((a) => a);
investmentData.set((a) => a);
bitcoinAddedData.set((a) => a);
averagePricePaidData.set((a) => a);
bitcoinPriceData.set((a) => a);
buyCountData.set((a) => a);
totalFeesPaidData.set((a) => a);
daysCountData.set((a) => a);
profitableDaysRatioData.set((a) => a);
unprofitableDaysRatioData.set((a) => a);
},
);
const serInvestedAmount = c("dollars", fd(investedAmount));
const serDaysCount = c("sky", f(daysCount));
const serSats = c("orange", f(sats));
const serBitcoin = c("orange", `~${f(bitcoin)}`);
const serBitcoinValue = c("amber", fd(bitcoinValue));
const serAveragePricePaid = c("lightDollars", fd(averagePricePaid));
const serRoi = c("yellow", fp(roi / 100));
const serDollars = c("offDollars", fd(dollars));
const serTotalFeesPaid = c("rose", fd(totalFeesPaid));
p1.innerHTML = `After exchanging ${serInvestedAmount} in the span of ${serDaysCount} days, you would have accumulated ${serSats} Satoshis (${serBitcoin} Bitcoin) worth today ${serBitcoinValue} at an average price of ${serAveragePricePaid} per Bitcoin with a return of investment of ${serRoi}, have ${serDollars} left and paid a total of ${serTotalFeesPaid} in fees.`;
const dayDiff = Math.floor(
utils.date.differenceBetween(new Date(), lastInvestDay),
);
const serDailyInvestment = c("offDollars", fd(dailyInvestment));
const setLastSatsAdded = c("bitcoin", 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`;
const serProfitableDaysRatio = c("green", fp(profitableDaysRatio));
const serUnprofitableDaysRatio = c(
"red",
fp(unprofitableDaysRatio),
);
p3.innerHTML = `You would've been ${serProfitableDaysRatio} of the time profitable and ${serUnprofitableDaysRatio} of the time unprofitable.`;
signals.createEffect(
() => 0.073,
(lowestAnnual4YReturn) => {
const serLowestAnnual4YReturn = c(
"cyan",
`${fp(lowestAnnual4YReturn)}`,
);
const lowestAnnual4YReturnPercentage = 1 + lowestAnnual4YReturn;
/**
* @param {number} power
*/
function bitcoinValueReturn(power) {
return (
bitcoinValue *
Math.pow(lowestAnnual4YReturnPercentage, power)
);
}
const bitcoinValueAfter4y = bitcoinValueReturn(4);
const serBitcoinValueAfter4y = c(
"purple",
fd(bitcoinValueAfter4y),
);
const bitcoinValueAfter10y = bitcoinValueReturn(10);
const serBitcoinValueAfter10y = c(
"fuchsia",
fd(bitcoinValueAfter10y),
);
const bitcoinValueAfter21y = bitcoinValueReturn(21);
const serBitcoinValueAfter21y = c(
"pink",
fd(bitcoinValueAfter21y),
);
/** @param {number} v */
p4.innerHTML = `The lowest annual return after 4 years has historically been ${serLowestAnnual4YReturn}.<br/>Using it as the baseline, your Bitcoin would be worth ${serBitcoinValueAfter4y} after 4 years, ${serBitcoinValueAfter10y} after 10 years and ${serBitcoinValueAfter21y} after 21 years.`;
},
);
totalInvestedAmountData.set((a) => a);
bitcoinValueData.set((a) => a);
bitcoinData.set((a) => a);
resultData.set((a) => a);
dollarsLeftData.set((a) => a);
totalValueData.set((a) => a);
investmentData.set((a) => a);
bitcoinAddedData.set((a) => a);
averagePricePaidData.set((a) => a);
bitcoinPriceData.set((a) => a);
buyCountData.set((a) => a);
totalFeesPaidData.set((a) => a);
daysCountData.set((a) => a);
profitableDaysRatioData.set((a) => a);
unprofitableDaysRatioData.set((a) => a);
},
);
});
});
});
}
-168
View File
@@ -1,168 +0,0 @@
import {
Accessor,
Setter,
} from "../../packages/solid-signals/2024-11-02/types/signals";
import {
DeepPartial,
BaselineStyleOptions,
CandlestickStyleOptions,
LineStyleOptions,
SeriesOptionsCommon,
IRange,
Time,
SingleValueData as _SingleValueData,
CandlestickData as _CandlestickData,
SeriesType,
ISeriesApi,
BaselineData,
} from "../../packages/lightweight-charts/v5.0.5/types";
import { AnyPossibleCohortId, Groups } from "../options";
type Color = () => string;
type ColorName = keyof Colors;
// TODO: Compute from VecId when displaying the Unit
// And write a checker when localhost, similar to the dup one
type Unit =
| ""
| "Bitcoin"
| "Coinblocks"
| "Count"
| "Date"
| "Dollars / (PetaHash / Second)"
| "ExaHash / Second"
| "Height"
| "Gigabytes"
| "Megabytes"
| "Percentage"
| "Ratio"
| "Satoshis"
| "Seconds"
| "Transactions"
| "US Dollars"
| "Virtual Bytes"
| "Weight";
interface PartialOption {
name: string;
}
interface PartialChartOption extends PartialOption {
title?: string;
unit?: string;
top?: SplitSeriesBlueprint[];
bottom?: SplitSeriesBlueprint[];
}
interface PartialSimulationOption extends PartialOption {
kind: "simulation";
title: string;
name: string;
}
interface PartialUrlOption extends PartialOption {
qrcode?: true;
url: () => string;
}
interface PartialOptionsGroup {
name: string;
tree: PartialOptionsTree;
}
type AnyPartialOption =
| PartialChartOption
| PartialSimulationOption
| PartialUrlOption;
type PartialOptionsTree = (AnyPartialOption | PartialOptionsGroup)[];
interface ProcessedOptionAddons {
id: string;
path: string[];
title: string;
}
type SimulationOption = PartialSimulationOption & ProcessedOptionAddons;
interface UrlOption extends PartialUrlOption, ProcessedOptionAddons {
kind: "url";
}
interface ChartOption
extends Omit<PartialChartOption, "title">,
ProcessedOptionAddons {
kind: "chart";
unit: string;
}
type Option = UrlOption | ChartOption | SimulationOption;
type OptionsTree = (Option | OptionsGroup)[];
interface OptionsGroup extends PartialOptionsGroup {
id: string;
tree: OptionsTree;
}
type OHLCTuple = [number, number, number, number];
interface Valued {
value: number;
}
interface Indexed {
index: number;
}
type ChartData<T> = T & Valued & Indexed;
type SingleValueData = ChartData<_SingleValueData>;
type CandlestickData = ChartData<_CandlestickData>;
type FetchedSource = string;
interface FetchedChunk {
id: number;
previous: string | null;
next: string | null;
}
interface Weighted {
weight: number;
}
type DatasetCandlestickData = ChartData<CandlestickData>;
// type NotFunction<T> = T extends Function ? never : T;
type DefaultCohortOption = CohortOption<AnyPossibleCohortId>;
interface CohortOption<Id extends AnyPossibleCohortId> {
name: string;
title: string;
datasetId: Id;
color: Color;
filenameAddon?: string;
}
type DefaultCohortOptions = CohortOptions<AnyPossibleCohortId>;
interface CohortOptions<Id extends AnyPossibleCohortId> {
name: string;
title: string;
list: CohortOption<Id>[];
}
interface RatioOption {
color: Color;
// valueDatasetPath: AnyDatasetPath;
// ratioDatasetPath: AnyDatasetPath;
title: string;
}
interface RatioOptions {
title: string;
list: RatioOption[];
}
// TODO: Remove
// Fetch last of each individually when in viewport
// type LastValues = Record<LastPath, number> | null;
@@ -1,3 +1,7 @@
//
// File auto-generated, any modification will be overwritten
//
/** @typedef {0} Height */
/** @typedef {1} Dateindex */
/** @typedef {2} Weekindex */
@@ -72,7 +76,6 @@ export function createVecIdToIndexes() {
"first-height": [Dateindex, Difficultyepoch, Halvingepoch],
"first-monthindex": [Quarterindex, Yearindex],
"first-multisigindex": [Height],
"first-open": [Weekindex, Monthindex, Quarterindex, Yearindex, Decadeindex, Difficultyepoch],
"first-opreturnindex": [Height],
"first-p2pk33index": [Height],
"first-p2pk65index": [Height],
@@ -91,9 +94,8 @@ export function createVecIdToIndexes() {
"fixed-timestamp": [Height],
halvingepoch: [Height, Halvingepoch],
height: [Addressindex, Height, Txindex],
high: [Dateindex, Height],
high: [Dateindex, Height, Weekindex, Monthindex, Quarterindex, Yearindex, Decadeindex, Difficultyepoch],
"high-in-cents": [Dateindex, Height],
"high-max": [Weekindex, Monthindex, Quarterindex, Yearindex, Decadeindex, Difficultyepoch],
"inputs-count": [Txindex],
"is-coinbase": [Txindex],
"is-explicitly-rbf": [Txindex],
@@ -105,13 +107,12 @@ export function createVecIdToIndexes() {
"last-txoutindex": [Txindex],
"last-yearindex": [Decadeindex],
locktime: [Txindex],
low: [Dateindex, Height],
low: [Dateindex, Height, Weekindex, Monthindex, Quarterindex, Yearindex, Decadeindex, Difficultyepoch],
"low-in-cents": [Dateindex, Height],
"low-min": [Weekindex, Monthindex, Quarterindex, Yearindex, Decadeindex, Difficultyepoch],
monthindex: [Dateindex, Monthindex],
ohlc: [Dateindex, Height, Weekindex, Monthindex, Quarterindex, Yearindex, Decadeindex, Difficultyepoch],
"ohlc-in-cents": [Dateindex, Height],
open: [Dateindex, Height],
open: [Dateindex, Height, Weekindex, Monthindex, Quarterindex, Yearindex, Decadeindex, Difficultyepoch],
"open-in-cents": [Dateindex, Height],
"outputs-count": [Txindex],
p2pk33addressbytes: [P2PK33index],
-4
View File
@@ -26,9 +26,5 @@
.chart {
flex: 1;
.lightweight-chart {
margin-left: var(--negative-main-padding);
}
}
}
+11 -9
View File
@@ -5,13 +5,8 @@
> div {
display: flex;
flex-direction: column;
gap: 1.5rem;
gap: 2rem;
padding: var(--main-padding);
> div {
display: flex;
flex-direction: column;
}
}
@media (max-width: 767px) {
@@ -60,16 +55,23 @@
display: block;
}
small {
font-size: var(--font-size-base);
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
display: block;
}
}
.chart {
flex-shrink: 0;
flex: none;
height: 400px;
.lightweight-chart {
margin-left: calc(var(--negative-main-padding) / 2);
margin-left: calc(var(--negative-main-padding) * 0.75);
fieldset {
margin-left: -0.5rem;
}
}
}
}
}