Compare commits

...

24 Commits

Author SHA1 Message Date
nym21 813b2481de release: v0.0.49 2025-06-11 17:51:31 +02:00
nym21 27b924ba61 cargo: set full version of crates 2025-06-11 17:51:11 +02:00
nym21 b40170b8ce websites: default: snapshot 2025-06-11 17:45:17 +02:00
nym21 8bfa9d2734 websites: default: snapshot 2025-06-11 11:25:25 +02:00
nym21 c7cf76d4a8 websites: default: snapshot 2025-06-10 18:54:18 +02:00
nym21 dfd2969b3e websites: default: snapshot 2025-06-09 17:58:26 +02:00
nym21 0e1866fe1d release: v0.0.48 2025-06-09 13:53:33 +02:00
nym21 b9ae46b913 readme: update 2025-06-09 13:53:09 +02:00
nym21 06e7284055 websites: default: snapshot 2025-06-09 13:05:03 +02:00
nym21 93289e8fca release: v0.0.47 2025-06-08 20:35:36 +02:00
nym21 130d5057d4 server: readme: add index-t-value documentation 2025-06-08 20:35:26 +02:00
nym21 be492d5084 server: add support for /api/X-to-Y + fix query cli + add meta api endpoints 2025-06-08 20:30:53 +02:00
nym21 e0bf1d736f query: add count param 2025-06-08 18:26:59 +02:00
nym21 5a6b71cbeb server: add ddos protection 2025-06-08 17:06:36 +02:00
nym21 e6934cd5e2 release: v0.0.46 2025-06-08 16:06:27 +02:00
nym21 b5aada0792 websites: mv sw to root 2025-06-08 16:05:21 +02:00
nym21 165ea83ac3 websites: update service worker 2025-06-08 13:03:37 +02:00
nym21 440a82dee4 release: v0.0.45 2025-06-08 09:11:31 +02:00
nym21 9c2d3e5e26 server: fix existing folder endpoints 2025-06-08 09:10:55 +02:00
nym21 6fb6abcbe5 release: v0.0.44 2025-06-07 18:53:51 +02:00
nym21 dc449dafd1 websites: default: up deps + fix css 2025-06-07 18:53:34 +02:00
nym21 ecdaeebbfb release: v0.0.43 2025-06-07 13:48:25 +02:00
nym21 fa958b59bd fetcher: support new api 2025-06-07 13:48:07 +02:00
nym21 fb3d8521cd computer: coinblocks fix overflow 2025-06-07 13:29:08 +02:00
84 changed files with 7320 additions and 6524 deletions
-2
View File
@@ -1,2 +0,0 @@
[build]
rustflags = ["-C", "target-cpu=native"]
Generated
+74 -74
View File
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "adler2"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aes"
@@ -146,9 +146,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-compression"
version = "0.4.23"
version = "0.4.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07"
checksum = "d615619615a650c571269c00dca41db04b9210037fa76ed8239f70404ab56985"
dependencies = [
"brotli",
"flate2",
@@ -388,7 +388,7 @@ dependencies = [
[[package]]
name = "brk"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"brk_cli",
"brk_computer",
@@ -407,7 +407,7 @@ dependencies = [
[[package]]
name = "brk_cli"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"bitcoincore-rpc",
"brk_computer",
@@ -432,7 +432,7 @@ dependencies = [
[[package]]
name = "brk_computer"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"bitcoin",
"bitcoincore-rpc",
@@ -453,7 +453,7 @@ dependencies = [
[[package]]
name = "brk_core"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"bincode",
"bitcoin",
@@ -474,7 +474,7 @@ dependencies = [
[[package]]
name = "brk_exit"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"brk_logger",
"ctrlc",
@@ -483,7 +483,7 @@ dependencies = [
[[package]]
name = "brk_fetcher"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"brk_core",
"brk_logger",
@@ -496,7 +496,7 @@ dependencies = [
[[package]]
name = "brk_indexer"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"bitcoin",
"bitcoincore-rpc",
@@ -514,7 +514,7 @@ dependencies = [
[[package]]
name = "brk_logger"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"color-eyre",
"env_logger",
@@ -524,7 +524,7 @@ dependencies = [
[[package]]
name = "brk_parser"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"bitcoin",
"bitcoincore-rpc",
@@ -539,7 +539,7 @@ dependencies = [
[[package]]
name = "brk_query"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"brk_computer",
"brk_core",
@@ -557,7 +557,7 @@ dependencies = [
[[package]]
name = "brk_server"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"axum",
"bitcoincore-rpc",
@@ -586,7 +586,7 @@ dependencies = [
[[package]]
name = "brk_state"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"bincode",
"brk_core",
@@ -600,7 +600,7 @@ dependencies = [
[[package]]
name = "brk_store"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"arc-swap",
"brk_core",
@@ -610,7 +610,7 @@ dependencies = [
[[package]]
name = "brk_vec"
version = "0.0.42"
version = "0.0.49"
dependencies = [
"arc-swap",
"brk_core",
@@ -722,9 +722,9 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "cfg_aliases"
@@ -757,18 +757,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.39"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.39"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
dependencies = [
"anstream",
"anstyle",
@@ -778,21 +778,21 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.32"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "color-eyre"
@@ -990,7 +990,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -1001,7 +1001,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -1042,7 +1042,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -1088,7 +1088,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -1171,9 +1171,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.1.1"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"crc32fast",
"libz-rs-sys",
@@ -1258,7 +1258,7 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi 0.11.1+wasi-snapshot-preview1",
]
[[package]]
@@ -1301,9 +1301,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.3"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
dependencies = [
"allocator-api2",
"equivalent",
@@ -1481,7 +1481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown 0.15.3",
"hashbrown 0.15.4",
"serde",
]
@@ -1547,7 +1547,7 @@ checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -1719,9 +1719,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "miniz_oxide"
version = "0.8.8"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
@@ -1747,7 +1747,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
@@ -1876,7 +1876,7 @@ checksum = "3bd3da01a295024fa79e3b4aba14b590d91256a274ff29cc5ee8f55183b2df24"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -1887,7 +1887,7 @@ checksum = "92e50218e74886659d1d13de8e6a4ff13c7e96924ed0017bc193a1feb8001b18"
dependencies = [
"allocator-api2",
"bumpalo",
"hashbrown 0.15.3",
"hashbrown 0.15.4",
"oxc_data_structures",
"rustc-hash",
]
@@ -1918,7 +1918,7 @@ dependencies = [
"phf",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -2245,7 +2245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca"
dependencies = [
"fixedbitset",
"hashbrown 0.15.3",
"hashbrown 0.15.4",
"indexmap 2.9.0",
"serde",
]
@@ -2280,7 +2280,7 @@ dependencies = [
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -2359,7 +2359,7 @@ dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -2378,7 +2378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b450dad8382b1b95061d5ca1eb792081fb082adf48c678791fe917509596d5f"
dependencies = [
"equivalent",
"hashbrown 0.15.3",
"hashbrown 0.15.4",
]
[[package]]
@@ -2515,9 +2515,9 @@ dependencies = [
[[package]]
name = "rustc-demangle"
version = "0.1.24"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
[[package]]
name = "rustc-hash"
@@ -2653,7 +2653,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -2726,7 +2726,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -2769,9 +2769,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "smallvec"
version = "1.15.0"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "smawk"
@@ -2826,9 +2826,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.101"
version = "2.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
checksum = "f6397daf94fa90f058bd0fd88429dd9e5738999cca8d701813c80723add80462"
dependencies = [
"proc-macro2",
"quote",
@@ -2862,7 +2862,7 @@ dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -2915,7 +2915,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -2982,7 +2982,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -3107,7 +3107,7 @@ checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -3167,9 +3167,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-width"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
[[package]]
name = "untrusted"
@@ -3238,9 +3238,9 @@ checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
@@ -3273,7 +3273,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
"wasm-bindgen-shared",
]
@@ -3295,7 +3295,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3336,7 +3336,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -3347,7 +3347,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -3458,9 +3458,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.10"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
dependencies = [
"memchr",
]
@@ -3497,7 +3497,7 @@ checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
@@ -3517,7 +3517,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.102",
]
[[package]]
+16 -16
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.42"
package.version = "0.0.49"
package.homepage = "https://bitcoinresearchkit.org"
package.repository = "https://github.com/bitcoinresearchkit/brk"
@@ -22,22 +22,22 @@ axum = "0.8.4"
bincode = { version = "2.0.1", features = ["serde"] }
bitcoin = { version = "0.32.6", features = ["serde"] }
bitcoincore-rpc = "0.19.0"
brk_cli = { version = "0", path = "crates/brk_cli" }
brk_computer = { version = "0", path = "crates/brk_computer" }
brk_core = { version = "0", path = "crates/brk_core" }
brk_exit = { version = "0", path = "crates/brk_exit" }
brk_fetcher = { version = "0", path = "crates/brk_fetcher" }
brk_indexer = { version = "0", path = "crates/brk_indexer" }
brk_logger = { version = "0", path = "crates/brk_logger" }
brk_parser = { version = "0", path = "crates/brk_parser" }
brk_query = { version = "0", path = "crates/brk_query" }
brk_server = { version = "0", path = "crates/brk_server" }
brk_state = { version = "0", path = "crates/brk_state" }
brk_store = { version = "0", path = "crates/brk_store" }
brk_vec = { version = "0", path = "crates/brk_vec" }
brk_cli = { version = "0.0.49", path = "crates/brk_cli" }
brk_computer = { version = "0.0.49", path = "crates/brk_computer" }
brk_core = { version = "0.0.49", path = "crates/brk_core" }
brk_exit = { version = "0.0.49", path = "crates/brk_exit" }
brk_fetcher = { version = "0.0.49", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.0.49", path = "crates/brk_indexer" }
brk_logger = { version = "0.0.49", path = "crates/brk_logger" }
brk_parser = { version = "0.0.49", path = "crates/brk_parser" }
brk_query = { version = "0.0.49", path = "crates/brk_query" }
brk_server = { version = "0.0.49", path = "crates/brk_server" }
brk_state = { version = "0.0.49", path = "crates/brk_state" }
brk_store = { version = "0.0.49", path = "crates/brk_store" }
brk_vec = { version = "0.0.49", path = "crates/brk_vec" }
byteview = "=0.6.1"
clap = { version = "4.5.39", features = ["string"] }
clap_derive = "4.5.32"
clap = { version = "4.5.40", features = ["string"] }
clap_derive = "4.5.40"
color-eyre = "0.6.5"
derive_deref = "1.1.1"
fjall = "2.11.0"
+18 -26
View File
@@ -31,28 +31,22 @@
</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 node, enabling users to gain deeper insights into the Bitcoin network.
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.
In other words it's an alternative to [Glassnode](https://glassnode.com), [mempool.space](https://mempool.space/) (soon) and [electrs](https://github.com/romanz/electrs) (soon) all in one package with a particular focus on simplicity and ease of use.
The toolkit can be used in various ways to accommodate as many needs as possible:
- **[Website](https://kibo.money)** \
Everyone is welcome to visit [kibo.money](https://kibo.money) which is the official showcase of the suite's capabilities and served by default when running BRK. \
Researchers and developers are free to use the API which endpoints documentation can be found [here](https://github.com/bitcoinresearchkit/brk/tree/main/crates/brk_server#endpoints). \
As a token of gratitude to the community and to stimulate curiosity, both the website and the API are entirely free, allowing anyone to use them.
- **[Website](https://bitcoinresearchkit.org)** \
Everyone is welcome to visit the official instance and showcase of the suite's capabilities. \
It has a wide range of functionalities including charts, tables and simulations which you can visit for free and without the need for an account. \
Also available at: [kibo.money](https://kibo.money) // [satonomics.xyz](https://satonomics.xyz)
- **[API](https://github.com/bitcoinresearchkit/brk/tree/main/crates/brk_server#endpoints)** \
Researchers and developers are free to use BRK's public API with ![Datasets variant count](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fbitcoinresearchkit.org%2Fapi%2Fvecs%2Fvariant-count&query=%24&style=flat&label=%20&color=white) dataset variants at your disposal. \
Just like the website, it's entirely free, with no authentication or rate-limiting.
- **[CLI](https://crates.io/crates/brk_cli)** \
Node runners are strongly encouraged to try out and self-host their own instance. \
A lot of effort has gone into making this as easy as possible. \
For more information visit: [`brk_cli`](https://crates.io/crates/brk_cli)
Node runners are strongly encouraged to try out and self-host their own instance using BRK's command line interface. \
The CLI has multiple cogs available for users to tweak to adapt to all situations with even the possibility for web developers to create their own custom website which could later on be added as an alternative front-end.
- **[Crates](https://crates.io/crates/brk)** \
Rust developers have access to a wide range crates, each built upon one another with its own specific purpose, enabling independent use and offering great flexibility.
PRs are welcome, especially if their goal is to introduce additional datasets.
@@ -78,21 +72,13 @@ In contrast, existing alternatives tend to be either [very costly](https://studi
- [`brk_store`](https://crates.io/crates/brk_store): A thin wrapper around [`fjall`](https://crates.io/crates/fjall)
- [`brk_vec`](https://crates.io/crates/brk_vec): A push-only, truncable, compressable, saveable Vec
## Acknowledgments
Deepest gratitude to the [Open Sats](https://opensats.org/) public charity. Their grant — from December 2024 to the present — has been critical in sustaining this project.
Heartfelt thanks go out to every donor on [Nostr](https://primal.net/p/npub1jagmm3x39lmwfnrtvxcs9ac7g300y3dusv9lgzhk2e4x5frpxlrqa73v44) and [Geyser.fund](https://geyser.fund/project/brk) whose support has ensured the availability of the [kibo.money](https://kibo.money) public instance.
## Hosting as a service
*Soon™*
If you'd like to have your own instance hosted for you please contact [hosting@bitcoinresearchkit.org](mailto:hosting@bitcoinresearchkit.org).
- 2 separate dedicated servers (1 GB/s each) with different ISPs and Cloudflare integration for enhanced performance and optimal availability
- 99.99% SLA
- Configurated for speed (`raw + eager`)
- Configured for speed
- Updates delivered at your convenience
- Direct communication for feature requests and support
- Bitcoin Core or Knots with desired version
@@ -101,6 +87,12 @@ If you'd like to have your own instance hosted for you please contact [hosting@b
Pricing: `0.01 BTC / month` *or* `0.1 BTC / year`
## Acknowledgments
Deepest gratitude to the [Open Sats](https://opensats.org/) public charity. Their grant — from December 2024 to the present — has been critical in sustaining this project.
Heartfelt thanks go out to every donor on [Nostr](https://primal.net/p/npub1jagmm3x39lmwfnrtvxcs9ac7g300y3dusv9lgzhk2e4x5frpxlrqa73v44) and [Geyser.fund](https://geyser.fund/project/brk) whose support has ensured the availability of the [kibo.money](https://kibo.money) public instance.
## Donate
[`bc1q09 8zsm89 m7kgyz e338vf ejhpdt 92ua9p 3peuve`](bitcoin:bc1q098zsm89m7kgyze338vfejhpdt92ua9p3peuve)
+9 -5
View File
@@ -1,4 +1,4 @@
use std::fs;
use std::{fs, thread};
use brk_core::{dot_brk_log_path, dot_brk_path};
use brk_query::Params as QueryArgs;
@@ -35,8 +35,12 @@ pub fn main() -> color_eyre::Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Run(args) => run(args),
Commands::Query(args) => query(args),
}
thread::Builder::new()
.stack_size(128 * 1024 * 1024)
.spawn(|| match cli.command {
Commands::Run(args) => run(args),
Commands::Query(args) => query(args),
})?
.join()
.unwrap()
}
+5 -3
View File
@@ -19,12 +19,14 @@ pub fn query(params: QueryParams) -> color_eyre::Result<()> {
let query = Query::build(&indexer, &computer);
let index = Index::try_from(params.index.as_str())?;
let ids = params.values.iter().map(|s| s.as_str()).collect::<Vec<_>>();
let from = params.from();
let to = params.to();
let format = params.format();
let res = query.search_and_format(index, &ids, params.from, params.to, params.format)?;
let res = query.search_and_format(index, &ids, from, to, format)?;
if params.format.is_some() {
if format.is_some() {
println!("{}", res);
} else {
println!(
+40 -48
View File
@@ -1,7 +1,7 @@
use std::{
fs,
path::{Path, PathBuf},
thread::{self, sleep},
thread::sleep,
time::Duration,
};
@@ -49,69 +49,61 @@ pub fn run(config: RunConfig) -> color_eyre::Result<()> {
Ok(())
};
let f = move || -> color_eyre::Result<()> {
let mut computer = Computer::new(&config.outputsdir(), config.fetcher(), format);
computer.import_stores(&indexer)?;
computer.import_vecs(&indexer, config.computation())?;
let mut computer = Computer::new(&config.outputsdir(), config.fetcher(), format);
computer.import_stores(&indexer)?;
computer.import_vecs(&indexer, config.computation())?;
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(async {
let server = if config.serve() {
let served_indexer = indexer.clone();
let served_computer = computer.clone();
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(async {
let server = if config.serve() {
let served_indexer = indexer.clone();
let served_computer = computer.clone();
let server = Server::new(served_indexer, served_computer, config.website())?;
let server = Server::new(served_indexer, served_computer, config.website())?;
let opt = Some(tokio::spawn(async move {
server.serve().await.unwrap();
}));
let opt = Some(tokio::spawn(async move {
server.serve().await.unwrap();
}));
sleep(Duration::from_secs(1));
sleep(Duration::from_secs(1));
opt
} else {
None
};
opt
} else {
None
};
if config.process() {
loop {
wait_for_synced_node()?;
if config.process() {
loop {
wait_for_synced_node()?;
let block_count = rpc.get_block_count()?;
let block_count = rpc.get_block_count()?;
info!("{} blocks found.", block_count + 1);
info!("{} blocks found.", block_count + 1);
let starting_indexes = indexer.index(&parser, rpc, &exit)?;
let starting_indexes = indexer.index(&parser, rpc, &exit)?;
computer.compute(&mut indexer, starting_indexes, &exit)?;
computer.compute(&mut indexer, starting_indexes, &exit)?;
if let Some(delay) = config.delay() {
sleep(Duration::from_secs(delay))
}
if let Some(delay) = config.delay() {
sleep(Duration::from_secs(delay))
}
info!("Waiting for new blocks...");
info!("Waiting for new blocks...");
while block_count == rpc.get_block_count()? {
sleep(Duration::from_secs(1))
}
while block_count == rpc.get_block_count()? {
sleep(Duration::from_secs(1))
}
}
}
if let Some(handle) = server {
handle.await.unwrap();
}
if let Some(handle) = server {
handle.await.unwrap();
}
Ok(())
})
};
thread::Builder::new()
.stack_size(128 * 1024 * 1024)
.spawn(f)?
.join()
.unwrap()
Ok(())
})
}
#[derive(Parser, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
@@ -136,7 +128,7 @@ pub struct RunConfig {
#[arg(short, long)]
computation: Option<Computation>,
/// Activate compression of datasets, set to true to save disk space or false if prioritize speed, default: true, saved
/// Activate compression of datasets, set to true to save disk space or false if prioritize speed, default: compressed, saved
#[arg(short, long, value_name = "FORMAT")]
format: Option<Format>,
+25 -16
View File
@@ -1,4 +1,4 @@
use std::{fs, path::Path};
use std::{fs, path::Path, thread};
use brk_core::Version;
use brk_exit::Exit;
@@ -44,22 +44,31 @@ impl Vecs {
) -> color_eyre::Result<Self> {
fs::create_dir_all(path)?;
let indexes = indexes::Vecs::forced_import(
path,
version + VERSION + Version::ZERO,
indexer,
computation,
format,
)?;
let (indexes, fetched) = thread::scope(|s| {
let indexes_handle = s.spawn(|| {
indexes::Vecs::forced_import(
path,
version + VERSION + Version::ZERO,
indexer,
computation,
format,
)
.unwrap()
});
let fetched = fetch.then(|| {
fetched::Vecs::forced_import(
path,
version + VERSION + Version::ZERO,
computation,
format,
)
.unwrap()
let fetch_handle = s.spawn(|| {
fetch.then(|| {
fetched::Vecs::forced_import(
path,
version + VERSION + Version::ZERO,
computation,
format,
)
.unwrap()
})
});
(indexes_handle.join().unwrap(), fetch_handle.join().unwrap())
});
Ok(Self {
@@ -54,8 +54,8 @@ pub struct Vecs {
pub height_to_satblocks_destroyed: EagerVec<Height, Sats>,
pub height_to_satdays_destroyed: EagerVec<Height, Sats>,
pub indexes_to_coinblocks_destroyed: ComputedVecsFromHeight<Bitcoin>,
pub indexes_to_coindays_destroyed: ComputedVecsFromHeight<Bitcoin>,
pub indexes_to_coinblocks_destroyed: ComputedVecsFromHeight<StoredF64>,
pub indexes_to_coindays_destroyed: ComputedVecsFromHeight<StoredF64>,
pub dateindex_to_adjusted_spent_output_profit_ratio: Option<EagerVec<DateIndex, StoredF32>>,
pub dateindex_to_realized_cap_30d_change: Option<EagerVec<DateIndex, Dollars>>,
pub dateindex_to_sell_side_risk_ratio: Option<EagerVec<DateIndex, StoredF32>>,
@@ -891,7 +891,7 @@ impl Vecs {
path,
&suffix("coinblocks_destroyed"),
true,
version + VERSION + Version::ONE,
version + VERSION + Version::TWO,
format,
StorableVecGeneatorOptions::default().add_sum(),
)?,
@@ -899,7 +899,7 @@ impl Vecs {
path,
&suffix("coindays_destroyed"),
true,
version + VERSION + Version::ONE,
version + VERSION + Version::TWO,
format,
StorableVecGeneatorOptions::default().add_sum(),
)?,
@@ -1839,9 +1839,10 @@ impl Vecs {
starting_indexes,
exit,
|v, _, _, starting_indexes, exit| {
v.compute_from_sats(
v.compute_transform(
starting_indexes.height,
&self.height_to_satblocks_destroyed,
|(i, v, ..)| (i, StoredF64::from(Bitcoin::from(v))),
exit,
)
},
@@ -1853,9 +1854,10 @@ impl Vecs {
starting_indexes,
exit,
|v, _, _, starting_indexes, exit| {
v.compute_from_sats(
v.compute_transform(
starting_indexes.height,
&self.height_to_satdays_destroyed,
|(i, v, ..)| (i, StoredF64::from(Bitcoin::from(v))),
exit,
)
},
+17 -1
View File
@@ -1,4 +1,7 @@
use std::ops::Add;
use std::{
fmt,
ops::{Add, Rem},
};
use serde::Serialize;
// use color_eyre::eyre::eyre;
@@ -77,3 +80,16 @@ impl CheckedSub for DateIndex {
self.0.checked_sub(rhs.0).map(Self)
}
}
impl Rem<usize> for DateIndex {
type Output = Self;
fn rem(self, rhs: usize) -> Self::Output {
Self(self.0 % rhs as u16)
}
}
impl fmt::Display for DateIndex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
+7 -3
View File
@@ -33,7 +33,9 @@ impl Version {
Self(self.0.swap_bytes())
}
pub fn validate(&self, path: &Path) -> Result<()> {
/// Ok(true) if existed and is same
/// Ok(false) if didn't exist
pub fn validate(&self, path: &Path) -> Result<bool> {
if let Ok(prev_version) = Version::try_from(path) {
if prev_version != *self {
if prev_version.swap_bytes() == *self {
@@ -44,9 +46,11 @@ impl Version {
expected: *self,
});
}
}
Ok(())
Ok(true)
} else {
Ok(false)
}
}
}
+6 -2
View File
@@ -1,11 +1,15 @@
use brk_core::Date;
use brk_fetcher::Fetcher;
use brk_core::{Date, Height};
use brk_fetcher::{BRK, Fetcher};
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
brk_logger::init(None);
let mut brk = BRK::default();
dbg!(brk.get_from_height(Height::new(900_000))?);
dbg!(brk.get_from_date(Date::new(2025, 6, 7))?);
let mut fetcher = Fetcher::import(None)?;
dbg!(fetcher.get_date(Date::new(2025, 6, 5))?);
+143
View File
@@ -0,0 +1,143 @@
use std::collections::BTreeMap;
use brk_core::{Cents, CheckedSub, Date, DateIndex, Height, OHLCCents};
use color_eyre::eyre::{ContextCompat, eyre};
use log::info;
use serde_json::Value;
use crate::{Close, Dollars, High, Low, Open, fetchers::retry};
#[derive(Default, Clone)]
pub struct BRK {
height_to_ohlc: BTreeMap<Height, Vec<OHLCCents>>,
dateindex_to_ohlc: BTreeMap<DateIndex, Vec<OHLCCents>>,
}
const API_URL: &str = "https://bitcoinresearchkit.org/api";
const RETRIES: usize = 10;
const CHUNK_SIZE: usize = 10_000;
impl BRK {
pub fn get_from_height(&mut self, height: Height) -> color_eyre::Result<OHLCCents> {
let key = height.checked_sub(height % CHUNK_SIZE).unwrap();
#[allow(clippy::map_entry)]
if !self.height_to_ohlc.contains_key(&key)
|| ((key + self.height_to_ohlc.get(&key).unwrap().len()) <= height)
{
self.height_to_ohlc.insert(
key,
Self::fetch_height_prices(key).inspect_err(|e| {
dbg!(e);
})?,
);
}
self.height_to_ohlc
.get(&key)
.unwrap()
.get(usize::from(height.checked_sub(key).unwrap()))
.cloned()
.ok_or(eyre!("Couldn't find height in kibo"))
}
fn fetch_height_prices(height: Height) -> color_eyre::Result<Vec<OHLCCents>> {
info!("Fetching Kibo height {height} prices...");
retry(
|_| {
let url = format!(
"{API_URL}/query?index=height&values=ohlc&from={}&to={}",
height,
height + CHUNK_SIZE
);
let body: Value = minreq::get(url).send()?.json()?;
body.as_array()
.context("Expect to be an array")?
.iter()
.map(Self::value_to_ohlc)
.collect::<Result<Vec<_>, _>>()
},
30,
RETRIES,
)
}
pub fn get_from_date(&mut self, date: Date) -> color_eyre::Result<OHLCCents> {
let dateindex = DateIndex::try_from(date)?;
let key = dateindex.checked_sub(dateindex % CHUNK_SIZE).unwrap();
#[allow(clippy::map_entry)]
if !self.dateindex_to_ohlc.contains_key(&key)
|| ((key + self.dateindex_to_ohlc.get(&key).unwrap().len()) <= dateindex)
{
self.dateindex_to_ohlc.insert(
key,
Self::fetch_date_prices(key).inspect_err(|e| {
dbg!(e);
})?,
);
}
self.dateindex_to_ohlc
.get(&key)
.unwrap()
.get(usize::from(dateindex.checked_sub(key).unwrap()))
.cloned()
.ok_or(eyre!("Couldn't find date in kibo"))
}
fn fetch_date_prices(dateindex: DateIndex) -> color_eyre::Result<Vec<OHLCCents>> {
info!("Fetching Kibo dateindex {dateindex} prices...");
retry(
|_| {
let url = format!(
"{API_URL}/query?index=dateindex&values=ohlc&from={}&to={}",
dateindex,
dateindex + CHUNK_SIZE
);
let body: Value = minreq::get(url).send()?.json()?;
body.as_array()
.context("Expect to be an array")?
.iter()
.map(Self::value_to_ohlc)
.collect::<Result<Vec<_>, _>>()
},
30,
RETRIES,
)
}
fn value_to_ohlc(value: &Value) -> color_eyre::Result<OHLCCents> {
let ohlc = value.as_array().context("Expect as_array to work")?;
let get_value = |index: usize| -> color_eyre::Result<_> {
Ok(Cents::from(Dollars::from(
ohlc.get(index)
.context("Expect index key to work")?
.as_f64()
.context("Expect as_f64 to work")?,
)))
};
Ok(OHLCCents::from((
Open::new(get_value(0)?),
High::new(get_value(1)?),
Low::new(get_value(2)?),
Close::new(get_value(3)?),
)))
}
pub fn clear(&mut self) {
self.height_to_ohlc.clear();
self.dateindex_to_ohlc.clear();
}
}
-155
View File
@@ -1,155 +0,0 @@
use std::{collections::BTreeMap, str::FromStr};
use brk_core::{CheckedSub, Date, Height, OHLCCents};
use color_eyre::eyre::{ContextCompat, eyre};
use log::info;
use serde_json::Value;
use crate::{Cents, Close, Dollars, High, Low, Open, fetchers::retry};
#[derive(Default, Clone)]
pub struct Kibo {
height_to_ohlc_vec: BTreeMap<Height, Vec<OHLCCents>>,
year_to_date_to_ohlc: BTreeMap<u16, BTreeMap<Date, OHLCCents>>,
}
const KIBO_OFFICIAL_URL: &str = "https://kibo.money/api";
const RETRIES: usize = 10;
impl Kibo {
pub fn get_from_height(&mut self, height: Height) -> color_eyre::Result<OHLCCents> {
let key = height.checked_sub(height % 10_000).unwrap_or_default();
#[allow(clippy::map_entry)]
if !self.height_to_ohlc_vec.contains_key(&key)
|| ((key + self.height_to_ohlc_vec.get(&key).unwrap().len()) <= height)
{
self.height_to_ohlc_vec.insert(
key,
Self::fetch_height_prices(key).inspect_err(|e| {
dbg!(e);
})?,
);
}
self.height_to_ohlc_vec
.get(&key)
.unwrap()
.get(usize::from(height.checked_sub(key).unwrap()))
.cloned()
.ok_or(eyre!("Couldn't find height in kibo"))
}
fn fetch_height_prices(height: Height) -> color_eyre::Result<Vec<OHLCCents>> {
info!("Fetching Kibo height {height} prices...");
retry(
|_| {
let url = format!("{KIBO_OFFICIAL_URL}/height-to-price?chunk={}", height);
let body: Value = minreq::get(url).send()?.json()?;
body.as_object()
.context("Expect to be an object")?
.get("dataset")
.context("Expect object to have dataset")?
.as_object()
.context("Expect to be an object")?
.get("map")
.context("Expect to have map")?
.as_array()
.context("Expect to be an array")?
.iter()
.map(Self::value_to_ohlc)
.collect::<Result<Vec<_>, _>>()
},
30,
RETRIES,
)
}
pub fn get_from_date(&mut self, date: &Date) -> color_eyre::Result<OHLCCents> {
let year = date.year();
#[allow(clippy::map_entry)]
if !self.year_to_date_to_ohlc.contains_key(&year)
|| self
.year_to_date_to_ohlc
.get(&year)
.unwrap()
.last_key_value()
.unwrap()
.0
<= date
{
self.year_to_date_to_ohlc
.insert(year, Self::fetch_date_prices(year)?);
}
self.year_to_date_to_ohlc
.get(&year)
.unwrap()
.get(date)
.cloned()
.ok_or(eyre!("Couldn't find date in kibo"))
}
fn fetch_date_prices(year: u16) -> color_eyre::Result<BTreeMap<Date, OHLCCents>> {
info!("Fetching Kibo date {year} prices...");
retry(
|_| {
let body: Value =
minreq::get(format!("{KIBO_OFFICIAL_URL}/date-to-price?chunk={}", year))
.send()?
.json()?;
body.as_object()
.context("Expect to be an object")?
.get("dataset")
.context("Expect object to have dataset")?
.as_object()
.context("Expect to be an object")?
.get("map")
.context("Expect to have map")?
.as_object()
.context("Expect to be an object")?
.iter()
.map(|(serialized_date, value)| -> color_eyre::Result<_> {
let date =
Date::from(jiff::civil::Date::from_str(serialized_date).unwrap());
Ok((date, Self::value_to_ohlc(value)?))
})
.collect::<Result<BTreeMap<_, _>, _>>()
},
30,
RETRIES,
)
}
fn value_to_ohlc(value: &Value) -> color_eyre::Result<OHLCCents> {
let ohlc = value.as_object().context("Expect as_object to work")?;
let get_value = |key: &str| -> color_eyre::Result<_> {
Ok(Cents::from(Dollars::from(
ohlc.get(key)
.context("Expect get key to work")?
.as_f64()
.context("Expect as_f64 to work")?,
)))
};
Ok(OHLCCents::from((
Open::new(get_value("open")?),
High::new(get_value("high")?),
Low::new(get_value("low")?),
Close::new(get_value("close")?),
)))
}
pub fn clear(&mut self) {
self.height_to_ohlc_vec.clear();
self.year_to_date_to_ohlc.clear();
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
mod binance;
// mod kibo;
mod brk;
mod kraken;
mod retry;
pub use binance::*;
// pub use kibo::*;
pub use brk::*;
pub use kraken::*;
use retry::*;
+26 -22
View File
@@ -10,7 +10,7 @@ use color_eyre::eyre::Error;
mod fetchers;
use fetchers::*;
pub use fetchers::*;
use log::info;
const TRIES: usize = 12 * 60;
@@ -19,7 +19,7 @@ const TRIES: usize = 12 * 60;
pub struct Fetcher {
binance: Binance,
kraken: Kraken,
// kibo: Kibo,
brk: BRK,
}
impl Fetcher {
@@ -31,7 +31,7 @@ impl Fetcher {
Ok(Self {
binance: Binance::init(hars_path),
kraken: Kraken::default(),
// kibo: Kibo::default(),
brk: BRK::default(),
})
}
@@ -46,6 +46,10 @@ impl Fetcher {
// eprintln!("{e}");
self.kraken.get_from_1d(&date)
})
.or_else(|_| {
// eprintln!("{e}");
self.brk.get_from_date(date)
})
.or_else(|e| {
sleep(Duration::from_secs(60));
@@ -94,28 +98,28 @@ impl Fetcher {
.get_from_1mn(timestamp, previous_timestamp)
.unwrap_or_else(|_report| {
// // eprintln!("{_report}");
// self.kibo.get_from_height(height).unwrap_or_else(|_report| {
// eprintln!("{_report}");
self.brk.get_from_height(height).unwrap_or_else(|_report| {
// eprintln!("{_report}");
sleep(Duration::from_secs(60));
sleep(Duration::from_secs(60));
if tries < TRIES {
self.clear();
if tries < TRIES {
self.clear();
info!("Retrying to fetch height prices...");
// dbg!((height, timestamp, previous_timestamp));
info!("Retrying to fetch height prices...");
// dbg!((height, timestamp, previous_timestamp));
return self
.get_height_(height, timestamp, previous_timestamp, tries + 1)
.unwrap();
}
return self
.get_height_(height, timestamp, previous_timestamp, tries + 1)
.unwrap();
}
info!("Failed to fetch height prices");
info!("Failed to fetch height prices");
let date = Date::from(timestamp);
// eprintln!("{e}");
panic!(
"
let date = Date::from(timestamp);
// eprintln!("{e}");
panic!(
"
Can't find the price for: height: {height} - date: {date}
1mn APIs are limited to the last 16 hours for Binance's and the last 10 hours for Kraken's
How to fix this:
@@ -130,8 +134,8 @@ How to fix this:
8. Export to a har file (if there is no explicit button, click on the cog button)
9. Move the file to 'parser/imports/binance.har'
"
)
// })
)
})
})
});
@@ -182,7 +186,7 @@ How to fix this:
pub fn clear(&mut self) {
self.binance.clear();
// self.kibo.clear();
self.brk.clear();
self.kraken.clear();
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ mod vec_trees;
pub use format::Format;
pub use index::Index;
pub use output::{Output, Value};
pub use params::Params;
pub use params::{Params, ParamsOpt};
pub use table::Tabled;
use vec_trees::VecTrees;
+105 -7
View File
@@ -1,6 +1,8 @@
use std::{fmt::Display, ops::Deref, str::FromStr};
use clap::builder::PossibleValuesParser;
use clap_derive::Parser;
use serde::Deserialize;
use serde::{Deserialize, Deserializer};
use serde_with::{OneOrMany, formats::PreferOne, serde_as};
use crate::{Format, Index};
@@ -17,15 +19,111 @@ pub struct Params {
#[serde_as(as = "OneOrMany<_, PreferOne>")]
/// Names of the values requested
pub values: Vec<String>,
#[clap(flatten)]
#[serde(flatten)]
pub rest: ParamsOpt,
}
// The macro creates custom deserialization code.
// You need to specify a function name and the field name of the flattened field.
serde_with::flattened_maybe!(deserialize_rest, "rest");
impl Deref for Params {
type Target = ParamsOpt;
fn deref(&self) -> &Self::Target {
&self.rest
}
}
impl From<((String, String), ParamsOpt)> for Params {
fn from(((index, id), rest): ((String, String), ParamsOpt)) -> Self {
Self {
index,
values: vec![id],
rest,
}
}
}
#[serde_as]
#[derive(Debug, Deserialize, Parser)]
pub struct ParamsOpt {
#[clap(short, long, allow_hyphen_values = true)]
#[serde(alias = "f")]
#[serde(default, alias = "f", deserialize_with = "de_unquote_i64")]
/// Inclusive starting index, if negative will be from the end
pub from: Option<i64>,
from: Option<i64>,
#[clap(short, long, allow_hyphen_values = true)]
#[serde(default, alias = "t")]
/// Inclusive ending index, if negative will be from the end
pub to: Option<i64>,
#[serde(default, alias = "t", deserialize_with = "de_unquote_i64")]
/// Exclusive ending index, if negative will be from the end, overrides 'count'
to: Option<i64>,
#[clap(short, long, allow_hyphen_values = true)]
#[serde(default, alias = "c", deserialize_with = "de_unquote_usize")]
/// Number of values
count: Option<usize>,
#[clap(short = 'F', long)]
/// Format of the output
pub format: Option<Format>,
format: Option<Format>,
}
impl ParamsOpt {
pub fn from(&self) -> Option<i64> {
self.from
}
pub fn to(&self) -> Option<i64> {
if self.to.is_none() {
if let Some(c) = self.count {
let c = c as i64;
if let Some(f) = self.from {
if f.is_positive() || f.abs() > c {
return Some(f + c);
}
} else {
return Some(c);
}
}
}
self.to
}
pub fn format(&self) -> Option<Format> {
self.format
}
}
fn de_unquote_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
where
D: Deserializer<'de>,
{
de_unquote(deserializer)
}
fn de_unquote_usize<'de, D>(deserializer: D) -> Result<Option<usize>, D::Error>
where
D: Deserializer<'de>,
{
de_unquote(deserializer)
}
fn de_unquote<'de, D, F>(deserializer: D) -> Result<Option<F>, D::Error>
where
D: Deserializer<'de>,
F: FromStr + Display,
<F as std::str::FromStr>::Err: Display,
{
let opt: Option<String> = Option::deserialize(deserializer)?;
let s = match opt {
None => return Ok(None),
Some(mut s) => {
// strip any leading/trailing quotes
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
s = s[1..s.len() - 1].to_string();
}
s
}
};
s.parse::<F>()
.map(Some)
.map_err(|e| serde::de::Error::custom(format!("cannot parse `{}` as type: {}", s, e)))
}
+45 -7
View File
@@ -41,22 +41,59 @@ The API uses `brk_query` and so inherites all of its features including formats.
### API
#### `GET /api/vecs/indexes`
#### [`GET /api/vecs/index-count`](https://bitcoinresearchkit.org/api/vecs/index-count)
Count of all possible indexes
#### [`GET /api/vecs/id-count`](https://bitcoinresearchkit.org/api/vecs/id-count)
Count of all possible ids
#### [`GET /api/vecs/variant-count`](https://bitcoinresearchkit.org/api/vecs/variant-count)
Count of all possible variants
#### [`GET /api/vecs/indexes`](https://bitcoinresearchkit.org/api/vecs/indexes)
A list of all possible vec indexes and their accepted variants
#### `GET /api/vecs/ids`
#### [`GET /api/vecs/ids`](https://bitcoinresearchkit.org/api/vecs/ids)
A list of all possible vec ids
#### `GET /api/vecs/id-to-indexes`
#### [`GET /api/vecs/variants`](https://bitcoinresearchkit.org/api/vecs/variants)
A list of all possible variants
#### [`GET /api/vecs/id-to-indexes`](https://bitcoinresearchkit.org/api/vecs/id-to-indexes)
A list of all possible vec ids and their supported vec indexes
#### `GET /api/vecs/index-to-ids`
#### [`GET /api/vecs/index-to-ids`](https://bitcoinresearchkit.org/api/vecs/index-to-ids)
A list of all possible vec indexes and their supported vec ids
#### `GET /api/{INDEX}-to-{ID}`
This endpoint retrieves data based on the specified vector index and id.
**Parameters:**
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | `signed int` | No | Inclusive starting index for pagination (default is 0). |
| `to` | `signed int` | No | Exclusive ending index for pagination (default is the total number of results). Overrides `count` |
| `count` | `unsigned int` | No | The number of values requested |
| `format` | `string` | No | The format of the response. Options include `json`, `csv`, `tsv`, or `md` (default is `json`). |
**Examples:**
```
GET /api/date-to-close
GET /date-to-close?from=-100
GET /date-to-close?count=100&format=csv
```
#### `GET /api/query`
This endpoint retrieves data based on the specified vector index and values.
@@ -67,8 +104,9 @@ This endpoint retrieves data based on the specified vector index and values.
| --- | --- | --- | --- |
| `index` | `VecIndex` | Yes | The vector index to query. |
| `values` | `VecId[]` | Yes | A comma or space-separated list of vector IDs to retrieve. |
| `from` | `unsigned int` | No | The starting index for pagination (default is 0). |
| `to` | `unsigned int` | No | The ending index for pagination (default is the total number of results). |
| `from` | `signed int` | No | Inclusive starting index for pagination (default is 0). |
| `to` | `signed int` | No | Exclusive ending index for pagination (default is the total number of results). Overrides `count` |
| `count` | `unsigned int` | No | The number of values requested |
| `format` | `string` | No | The format of the response. Options include `json`, `csv`, `tsv`, or `md` (default is `json`). |
**Examples:**
@@ -80,7 +118,7 @@ GET /api/query?index=week&values=ohlc,block-interval-average&from=0&to=20&format
### Meta
#### `GET /version`
#### [`GET /version`](https://bitcoinresearchkit.org/version)
The version of the server and thus BRK.
+90 -24
View File
@@ -1,18 +1,20 @@
use std::collections::BTreeMap;
use axum::{
Router,
extract::State,
Json, Router,
extract::{Path, Query, State},
http::HeaderMap,
response::{IntoResponse, Redirect, Response},
routing::get,
};
use brk_query::{Params, ParamsOpt};
use super::AppState;
mod explorer;
mod query;
pub use query::DTS;
pub use query::Bridge;
pub trait ApiRoutes {
fn add_api_routes(self) -> Self;
@@ -20,24 +22,29 @@ pub trait ApiRoutes {
impl ApiRoutes for Router<AppState> {
fn add_api_routes(self) -> Self {
self.route(
"/api",
get(|| async {
Redirect::permanent(
"https://github.com/bitcoinresearchkit/brk/tree/main/crates/brk_server#api",
)
}),
)
.route("/api/query", get(query::handler))
.route("/api/vecs/ids", get(vecids_handler))
.route("/api/vecs/indexes", get(vecindexes_handler))
.route("/api/vecs/id-to-indexes", get(vecid_to_vecindexes_handler))
.route("/api/vecs/index-to-ids", get(vecindex_to_vecids_handler))
self.route("/api/query", get(query::handler))
.route("/api/vecs/id-count", get(id_count_handler))
.route("/api/vecs/index-count", get(index_count_handler))
.route("/api/vecs/variant-count", get(variant_count_handler))
.route("/api/vecs/ids", get(ids_handler))
.route("/api/vecs/indexes", get(indexes_handler))
.route("/api/vecs/variants", get(variants_handler))
.route("/api/vecs/id-to-indexes", get(id_to_indexes_handler))
.route("/api/vecs/index-to-ids", get(index_to_ids_handler))
.route("/api/{variant}", get(variant_handler))
.route(
"/api",
get(|| async {
Redirect::temporary(
"https://github.com/bitcoinresearchkit/brk/tree/main/crates/brk_server#api",
)
}),
)
}
}
pub async fn vecids_handler(State(app_state): State<AppState>) -> Response {
axum::Json(
pub async fn ids_handler(State(app_state): State<AppState>) -> Response {
Json(
app_state
.query
.vec_trees
@@ -48,8 +55,29 @@ pub async fn vecids_handler(State(app_state): State<AppState>) -> Response {
.into_response()
}
pub async fn vecindexes_handler(State(app_state): State<AppState>) -> Response {
axum::Json(
pub async fn variant_count_handler(State(app_state): State<AppState>) -> Response {
Json(
app_state
.query
.vec_trees
.index_to_id_to_vec
.values()
.map(|tree| tree.len())
.sum::<usize>(),
)
.into_response()
}
pub async fn id_count_handler(State(app_state): State<AppState>) -> Response {
Json(app_state.query.vec_trees.id_to_index_to_vec.keys().count()).into_response()
}
pub async fn index_count_handler(State(app_state): State<AppState>) -> Response {
Json(app_state.query.vec_trees.index_to_id_to_vec.keys().count()).into_response()
}
pub async fn indexes_handler(State(app_state): State<AppState>) -> Response {
Json(
app_state
.query
.vec_trees
@@ -61,10 +89,48 @@ pub async fn vecindexes_handler(State(app_state): State<AppState>) -> Response {
.into_response()
}
pub async fn vecid_to_vecindexes_handler(State(app_state): State<AppState>) -> Response {
axum::Json(app_state.query.vec_trees.serialize_id_to_index_to_vec()).into_response()
pub async fn variants_handler(State(app_state): State<AppState>) -> Response {
Json(
app_state
.query
.vec_trees
.index_to_id_to_vec
.iter()
.flat_map(|(index, id_to_vec)| {
let index_ser = index.serialize_long();
id_to_vec
.keys()
.map(|id| format!("{}-to-{}", index_ser, id))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>(),
)
.into_response()
}
pub async fn vecindex_to_vecids_handler(State(app_state): State<AppState>) -> Response {
axum::Json(app_state.query.vec_trees.serialize_index_to_id_to_vec()).into_response()
pub async fn id_to_indexes_handler(State(app_state): State<AppState>) -> Response {
Json(app_state.query.vec_trees.serialize_id_to_index_to_vec()).into_response()
}
pub async fn index_to_ids_handler(State(app_state): State<AppState>) -> Response {
Json(app_state.query.vec_trees.serialize_index_to_id_to_vec()).into_response()
}
const TO_SEPARATOR: &str = "-to-";
pub async fn variant_handler(
headers: HeaderMap,
Path(variant): Path<String>,
Query(params_opt): Query<ParamsOpt>,
state: State<AppState>,
) -> Response {
let mut split = variant.split(TO_SEPARATOR);
let params = Params::from((
(
split.next().unwrap().to_string(),
split.collect::<Vec<_>>().join(TO_SEPARATOR),
),
params_opt,
));
query::handler(headers, Query(params), state).await
}
@@ -2,17 +2,17 @@ use std::{fs, io, path::Path};
use brk_query::{Index, Query};
use crate::Website;
use crate::{VERSION, Website};
const SCRIPTS: &str = "scripts";
#[allow(clippy::upper_case_acronyms)]
pub trait DTS {
fn generate_dts_file(&self, website: Website, websites_path: &Path) -> io::Result<()>;
pub trait Bridge {
fn generate_bridge_file(&self, website: Website, websites_path: &Path) -> io::Result<()>;
}
impl DTS for Query<'static> {
fn generate_dts_file(&self, website: Website, websites_path: &Path) -> io::Result<()> {
impl Bridge for Query<'static> {
fn generate_bridge_file(&self, website: Website, websites_path: &Path) -> io::Result<()> {
if website.is_none() {
return Ok(());
}
@@ -31,10 +31,16 @@ impl DTS for Query<'static> {
let indexes = Index::all();
let mut contents = "//
let mut contents = format!(
"//
// File auto-generated, any modifications will be overwritten
//\n\n"
.to_string();
//
export const VERSION = \"v{}\";
",
VERSION
);
contents += &indexes
.iter()
@@ -57,7 +63,7 @@ impl DTS for Query<'static> {
contents += "\n\nexport function createVecIdToIndexes() {\n";
contents += "\n\n return /** @type {const} */ ({\n";
contents += " return /** @type {const} */ ({\n";
self.vec_trees
.id_to_index_to_vec
+30 -7
View File
@@ -5,14 +5,18 @@ use axum::{
response::{IntoResponse, Response},
};
use brk_query::{Format, Index, Output, Params};
use brk_vec::{CollectableVec, StoredVec};
use color_eyre::eyre::eyre;
use crate::traits::{HeaderMapExtended, ModifiedState, ResponseExtended};
use super::AppState;
mod dts;
mod bridge;
pub use dts::*;
pub use bridge::*;
const MAX_WEIGHT: usize = 320_000;
pub async fn handler(
headers: HeaderMap,
@@ -33,11 +37,9 @@ pub async fn handler(
fn req_to_response_res(
headers: HeaderMap,
AxumQuery(Params {
format,
from,
index,
to,
values,
rest,
}): AxumQuery<Params>,
AppState { query, .. }: AppState,
) -> color_eyre::Result<Response> {
@@ -48,6 +50,27 @@ fn req_to_response_res(
&values.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
);
if vecs.is_empty() {
return Ok(Json(vec![] as Vec<usize>).into_response());
}
let from = rest.from();
let to = rest.to();
let format = rest.format();
let weight = vecs
.iter()
.map(|(_, v)| {
let len = v.len();
let count = StoredVec::<usize, usize>::range_count(from, to, len);
count * v.value_type_to_size_of()
})
.sum::<usize>();
if weight > MAX_WEIGHT {
return Err(eyre!("Request is too heavy, max weight is {MAX_WEIGHT}"));
}
let mut date_modified_opt = None;
if to.is_none() {
@@ -75,8 +98,8 @@ fn req_to_response_res(
Output::TSV(s) => s.into_response(),
Output::Json(v) => match v {
brk_query::Value::Single(v) => Json(v).into_response(),
brk_query::Value::List(l) => Json(l).into_response(),
brk_query::Value::Matrix(m) => Json(m).into_response(),
brk_query::Value::List(v) => Json(v).into_response(),
brk_query::Value::Matrix(v) => Json(v).into_response(),
},
Output::MD(s) => s.into_response(),
};
+1 -1
View File
@@ -43,7 +43,7 @@ fn any_handler(
let mut path = website_path.join(&path);
if !path.exists() {
if !path.exists() || path.is_dir() {
if path.extension().is_some() {
let mut response: Response<Body> = (
StatusCode::INTERNAL_SERVER_ERROR,
+4 -3
View File
@@ -10,7 +10,7 @@ use std::{
time::Duration,
};
use api::{ApiRoutes, DTS};
use api::{ApiRoutes, Bridge};
use axum::{
Json, Router,
body::Body,
@@ -45,10 +45,11 @@ pub struct AppState {
websites_path: Option<PathBuf>,
}
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
const DEV_PATH: &str = "../..";
const DOWNLOADS: &str = "downloads";
const WEBSITES: &str = "websites";
const VERSION: &str = env!("CARGO_PKG_VERSION");
pub struct Server(AppState);
@@ -88,7 +89,7 @@ impl Server {
downloaded_websites_path
};
query.generate_dts_file(website, websites_path.as_path())?;
query.generate_bridge_file(website, websites_path.as_path())?;
Some(websites_path)
} else {
+1
View File
@@ -13,6 +13,7 @@ pub trait AnyVec: Send + Sync {
}
fn modified_time(&self) -> Result<Duration>;
fn index_type_to_string(&self) -> String;
fn value_type_to_size_of(&self) -> usize;
}
pub trait AnyIterableVec<I, T>: AnyVec {
+7 -1
View File
@@ -27,13 +27,19 @@ where
#[inline]
fn i64_to_usize(i: i64, len: usize) -> usize {
if i >= 0 {
i as usize
(i as usize).min(len)
} else {
let v = len as i64 + i;
if v < 0 { 0 } else { v as usize }
}
}
fn range_count(from: Option<i64>, to: Option<i64>, len: usize) -> usize {
let from = from.map(|i| Self::i64_to_usize(i, len));
let to = to.map(|i| Self::i64_to_usize(i, len));
(from.unwrap_or_default()..to.unwrap_or(len)).count()
}
#[doc(hidden)]
fn collect_signed_range(&self, from: Option<i64>, to: Option<i64>) -> Result<Vec<T>> {
let len = self.len();
@@ -361,6 +361,11 @@ where
fn index_type_to_string(&self) -> String {
I::to_string()
}
#[inline]
fn value_type_to_size_of(&self) -> usize {
size_of::<T>()
}
}
impl<I, T> Clone for CompressedVec<I, T> {
+5
View File
@@ -255,6 +255,11 @@ where
ComputedVec::LazyFrom3(v) => v.modified_time(),
}
}
#[inline]
fn value_type_to_size_of(&self) -> usize {
size_of::<T>()
}
}
pub enum ComputedVecIterator<'a, I, T, S1I, S1T, S2I, S2T, S3I, S3T> {
+7 -2
View File
@@ -1116,12 +1116,12 @@ impl EagerVec<DateIndex, Dollars> {
exit: &Exit,
) -> Result<()> {
self.validate_computed_version_or_reset_file(
Version::ZERO + self.inner.version() + stacks.version(),
Version::ONE + self.inner.version() + stacks.version(),
)?;
let index = max_from.min(DateIndex::from(self.len()));
let first_price_date = DateIndex::try_from(Date::new(2010, 8, 16)).unwrap();
let first_price_date = DateIndex::try_from(Date::new(2010, 7, 12)).unwrap();
stacks.iter_at(index).try_for_each(|(i, stack)| {
let stack = stack.into_inner();
@@ -1298,6 +1298,11 @@ where
fn index_type_to_string(&self) -> String {
I::to_string()
}
#[inline]
fn value_type_to_size_of(&self) -> usize {
size_of::<T>()
}
}
impl<I, T> AnyIterableVec<I, T> for EagerVec<I, T>
+5
View File
@@ -127,6 +127,11 @@ where
fn index_type_to_string(&self) -> String {
I::to_string()
}
#[inline]
fn value_type_to_size_of(&self) -> usize {
size_of::<T>()
}
}
pub trait AnyIndexedVec: AnyVec {
+5
View File
@@ -146,6 +146,11 @@ where
fn modified_time(&self) -> Result<std::time::Duration> {
self.source.modified_time()
}
#[inline]
fn value_type_to_size_of(&self) -> usize {
size_of::<T>()
}
}
impl<I, T, S1I, S1T> AnyIterableVec<I, T> for LazyVecFrom1<I, T, S1I, S1T>
+5
View File
@@ -194,6 +194,11 @@ where
.modified_time()?
.min(self.source2.modified_time()?))
}
#[inline]
fn value_type_to_size_of(&self) -> usize {
size_of::<T>()
}
}
impl<I, T, S1I, S1T, S2I, S2T> AnyIterableVec<I, T> for LazyVecFrom2<I, T, S1I, S1T, S2I, S2T>
+5
View File
@@ -229,6 +229,11 @@ where
.min(self.source2.modified_time()?)
.min(self.source3.modified_time()?))
}
#[inline]
fn value_type_to_size_of(&self) -> usize {
size_of::<T>()
}
}
impl<I, T, S1I, S1T, S2I, S2T, S3I, S3T> AnyIterableVec<I, T>
+9 -2
View File
@@ -48,8 +48,10 @@ where
fs::create_dir_all(path)?;
let version_path = Self::path_version_(path);
version.validate(version_path.as_ref())?;
version.write(version_path.as_ref())?;
if !version.validate(version_path.as_ref())? {
version.write(version_path.as_ref())?;
}
let file = Self::open_file_(Self::path_vec_(path).as_path())?;
let mmap = Arc::new(ArcSwap::new(Self::new_mmap(file)?));
@@ -195,6 +197,11 @@ where
fn index_type_to_string(&self) -> String {
I::to_string()
}
#[inline]
fn value_type_to_size_of(&self) -> usize {
size_of::<T>()
}
}
impl<I, T> Clone for RawVec<I, T> {
+5
View File
@@ -149,6 +149,11 @@ where
StoredVec::Compressed(v) => v.name(),
}
}
#[inline]
fn value_type_to_size_of(&self) -> usize {
size_of::<T>()
}
}
#[derive(Debug)]
+24 -53
View File
@@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>kibo.money</title>
<meta
name="description"
content="An open source Bitcoin Core data extractor and visualizer"
@@ -649,17 +648,9 @@
height: 100%;
display: flex;
flex-direction: column;
gap: 2rem;
padding-bottom: var(--bottom-area);
}
nav {
header {
display: flex;
justify-content: center;
}
}
sup {
opacity: 0.5;
margin-left: 0.25rem;
@@ -728,6 +719,7 @@
*/
.tree {
margin-top: -0.125rem;
user-select: none;
-webkit-user-select: none;
@@ -764,7 +756,7 @@
text-transform: uppercase;
& + * {
margin-top: 1.5rem;
margin-top: 1.25rem;
}
> details,
@@ -800,14 +792,6 @@
}
}
search {
ul {
display: flex;
flex-direction: column;
gap: 1rem;
}
}
#share-div {
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
@@ -848,16 +832,12 @@
}
search {
> #search-no-input-text {
color: var(--off-color);
gap: 1rem;
&:has(+ ul li) {
display: none;
}
> button {
color: var(--color);
}
ul {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
}
@@ -887,7 +867,7 @@
bottom: 0;
left: 0;
right: 0;
z-index: 20;
z-index: 10;
pointer-events: none;
}
.shadow-left {
@@ -901,7 +881,7 @@
left: 0;
top: 0;
bottom: 0;
z-index: 40 !important;
z-index: 50;
pointer-events: none;
}
.shadow-right {
@@ -915,7 +895,7 @@
right: 0;
top: 0;
bottom: 0;
z-index: 10;
z-index: 30;
pointer-events: none;
}
@@ -1142,10 +1122,10 @@
/** @param {boolean} dark */
function updateThemeColor(dark) {
const backgroundColor = getComputedStyle(
const theme = getComputedStyle(
window.document.documentElement,
).getPropertyValue(dark ? "--black" : "--white");
themeColor.content = backgroundColor;
themeColor.content = theme;
}
updateThemeColor(preferredColorSchemeMatchMedia.matches);
@@ -1160,9 +1140,8 @@
window.document.documentElement.dataset.display = "standalone";
}
console.log(navigator);
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/scripts/service-worker.js");
navigator.serviceWorker.register("/service-worker.js");
}
</script>
@@ -1535,29 +1514,11 @@
</head>
<body>
<main id="main">
<script>
// Prevent width jumping
const savedWidth = localStorage.getItem("bar-width");
if (savedWidth) {
const main = window.document.getElementById("main");
if (!main) throw "Should exist";
main.style.width = `${savedWidth}px`;
}
</script>
<div class="shadow-top"></div>
<div class="shadow-bottom"></div>
<div id="resize-bar"></div>
<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>
<nav id="nav" hidden></nav>
<search id="search" hidden>
<header>
@@ -1625,5 +1586,15 @@
<a id="share-anchor" href="/"></a>
</div>
</div>
<script>
// Prevent width jumping
const savedWidth = localStorage.getItem("bar-width");
if (savedWidth) {
const main = window.document.getElementById("main");
if (!main) throw "Should exist";
main.style.width = `${savedWidth}px`;
}
</script>
</body>
</html>
+2 -1
View File
@@ -3,7 +3,8 @@
"checkJs": true,
"strict": true,
"target": "ESNext",
"module": "ESNext"
"module": "ESNext",
"skipLibCheck": true
},
"exclude": ["assets", "packages", "ignore"]
}
File diff suppressed because one or more lines are too long
-241
View File
@@ -1,241 +0,0 @@
declare module "lean-qr" {
interface ImageDataLike {
readonly data: Uint8ClampedArray;
}
interface Context2DLike<DataT extends ImageDataLike> {
createImageData(width: number, height: number): DataT;
putImageData(data: DataT, x: number, y: number): void;
}
interface CanvasLike<DataT extends ImageDataLike> {
width: number;
height: number;
getContext(type: "2d"): Context2DLike<DataT> | null;
}
export type RGBA = readonly [number, number, number, number?];
export interface Bitmap1D {
push(value: number, bits: number): void;
}
export interface StringOptions {
on?: string;
off?: string;
lf?: string;
padX?: number;
padY?: number;
}
export interface ImageDataOptions {
on?: RGBA;
off?: RGBA;
padX?: number;
padY?: number;
}
export interface Bitmap2D {
readonly size: number;
get(x: number, y: number): boolean;
toString(options?: Readonly<StringOptions>): string;
toImageData<DataT extends ImageDataLike>(
context: Context2DLike<DataT>,
options?: Readonly<ImageDataOptions>,
): DataT;
toDataURL(
options?: Readonly<
ImageDataOptions & {
type?: `image/${string}`;
scale?: number;
}
>,
): string;
toCanvas(
canvas: CanvasLike<ImageDataLike>,
options?: Readonly<ImageDataOptions>,
): void;
}
export type Mask = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
export type Mode = (data: Bitmap1D, version: number) => void;
export interface ModeFactory {
(value: string): Mode;
test(string: string): boolean;
est(value: string, version: number): number;
eci?: number;
}
interface ModeAutoOptions {
modes?: ReadonlyArray<ModeFactory>;
}
export const mode: Readonly<{
auto(value: string, options?: Readonly<ModeAutoOptions>): Mode;
multi(...modes: ReadonlyArray<Mode>): Mode;
eci(id: number): Mode;
numeric: ModeFactory;
alphaNumeric: ModeFactory;
bytes(data: Uint8Array | ReadonlyArray<number>): Mode;
ascii: ModeFactory;
iso8859_1: ModeFactory;
shift_jis: ModeFactory;
utf8: ModeFactory;
}>;
type Correction = number & { readonly _: unique symbol };
export const correction: Readonly<{
min: Correction;
L: Correction;
M: Correction;
Q: Correction;
H: Correction;
max: Correction;
}>;
export interface GenerateOptions extends ModeAutoOptions {
minCorrectionLevel?: Correction;
maxCorrectionLevel?: Correction;
minVersion?: number;
maxVersion?: number;
mask?: null | Mask;
trailer?: number;
}
export type GenerateFn = (
data: Mode | string,
options?: Readonly<GenerateOptions>,
) => Bitmap2D;
interface Generate extends GenerateFn {
with(...modes: ReadonlyArray<ModeFactory>): GenerateFn;
}
export const generate: Generate;
}
declare module "lean-qr/extras/svg" {
import type { Bitmap2D } from "lean-qr";
export interface SVGOptions {
on?: string;
off?: string;
padX?: number;
padY?: number;
width?: number | null;
height?: number | null;
scale?: number;
}
export const toSvgPath: (code: Bitmap2D) => string;
export const toSvg: (
code: Bitmap2D,
target: Document | SVGElement,
options?: Readonly<SVGOptions>,
) => SVGElement;
export const toSvgSource: (
code: Bitmap2D,
options?: Readonly<SVGOptions & { xmlDeclaration?: boolean }>,
) => string;
export type toSvgDataURLFn = (
code: Bitmap2D,
options?: Readonly<SVGOptions>,
) => string;
export const toSvgDataURL: toSvgDataURLFn;
}
declare module "lean-qr/extras/node_export" {
import type { RGBA, Bitmap2D } from "lean-qr";
export interface PNGOptions {
on?: RGBA;
off?: RGBA;
padX?: number;
padY?: number;
scale?: number;
}
export const toPngBuffer: (
code: Bitmap2D,
options?: Readonly<PNGOptions>,
) => Uint8Array;
export const toPngDataURL: (
code: Bitmap2D,
options?: Readonly<PNGOptions>,
) => string;
}
declare module "lean-qr/extras/react" {
import type { ImageDataOptions, GenerateOptions, GenerateFn } from "lean-qr";
import type { SVGOptions, toSvgDataURLFn } from "lean-qr/extras/svg";
export interface AsyncFramework<T> {
createElement: (
type: "canvas",
props: {
ref: any;
style: { imageRendering: "pixelated" };
className: string;
},
) => T;
useRef<T>(initialValue: T | null): { readonly current: T | null };
useEffect(fn: () => void | (() => void), deps: unknown[]): void;
}
interface QRComponentProps {
content: string;
className?: string;
}
export interface AsyncQRComponentProps
extends ImageDataOptions,
GenerateOptions,
QRComponentProps {}
export type AsyncQRComponent<T> = (
props: Readonly<AsyncQRComponentProps>,
) => T;
export const makeAsyncComponent: <T>(
framework: Readonly<AsyncFramework<T>>,
generate: GenerateFn,
defaultProps?: Readonly<Partial<AsyncQRComponentProps>>,
) => AsyncQRComponent<T>;
export interface SyncFramework<T> {
createElement: (
type: "img",
props: {
src: string;
style: { imageRendering: "pixelated" };
className: string;
},
) => T;
useMemo<T>(fn: () => T, deps: unknown[]): T;
}
export interface SyncQRComponentProps
extends SVGOptions,
GenerateOptions,
QRComponentProps {}
export type SyncQRComponent<T> = (props: Readonly<SyncQRComponentProps>) => T;
export const makeSyncComponent: <T>(
framework: Readonly<SyncFramework<T>>,
generate: GenerateFn,
toSvgDataURL: toSvgDataURLFn,
defaultProps?: Readonly<Partial<SyncQRComponentProps>>,
) => SyncQRComponent<T>;
}
declare module "lean-qr/extras/errors" {
export const readError: (error: unknown) => string;
}
File diff suppressed because one or more lines are too long
+491
View File
@@ -0,0 +1,491 @@
declare module "lean-qr" {
interface ImageDataLike {
readonly data: Uint8ClampedArray;
}
interface Context2DLike<DataT extends ImageDataLike> {
createImageData(width: number, height: number): DataT;
putImageData(data: DataT, x: number, y: number): void;
}
interface CanvasLike<DataT extends ImageDataLike> {
width: number;
height: number;
getContext(type: "2d"): Context2DLike<DataT> | null;
}
/**
* A colour in `[red, green, blue, alpha]` format (all values from 0 to 255).
* If alpha is omitted, it is assumed to be 255 (opaque).
*/
export type RGBA = readonly [number, number, number, number?];
export interface Bitmap1D {
/**
* Appends a sequence of bits.
*
* @param value an integer containing the bits to append (big endian).
* @param bits the number of bits to read from `value`. Must be between 1 and 24.
*/
push(value: number, bits: number): void;
}
export interface StringOptions {
/** the text to use for modules which are 'on' (typically black) */
on?: string;
/** the text to use for modules which are 'off' (typically white) */
off?: string;
/** the text to use for linefeeds between rows */
lf?: string;
/** the padding to apply on the left and right of the output (populated with 'off' modules) */
padX?: number;
/** the padding to apply on the top and bottom of the output (populated with 'off' modules) */
padY?: number;
}
export interface ImageDataOptions {
/** the colour to use for modules which are 'on' (typically black) */
on?: RGBA;
/** the colour to use for modules which are 'off' (typically white) */
off?: RGBA;
/** the padding to apply on the left and right of the output (filled with 'off') */
padX?: number;
/** the padding to apply on the top and bottom of the output (filled with 'off') */
padY?: number;
}
export interface Bitmap2D {
/** the width / height of the QR code in modules (excluding any padding) */
readonly size: number;
/**
* Read the state of a module from the QR code.
*
* @param x the x coordinate to read. Can be negative / out of bounds.
* @param y the y coordinate to read. Can be negative / out of bounds.
* @returns true if the requested module is set (i.e. typically black)
*/
get(x: number, y: number): boolean;
/**
* Generate a string containing the QR code, suitable for displaying in a
* terminal environment. Generally, you should customise on and off to use
* the ANSI escapes of your target terminal for better rendering.
*
* @param options optional configuration for the display.
*/
toString(options?: Readonly<StringOptions>): string;
/**
* Generate image data containing the QR code, at a scale of 1 pixel per
* module. Use this if you need more control than toCanvas allows.
*
* @param context a context to use for creating the image data.
* @param options optional configuration for the display.
*/
toImageData<DataT extends ImageDataLike>(
context: Context2DLike<DataT>,
options?: Readonly<ImageDataOptions>
): DataT;
/**
* Generate a `data:image/*` URL for the QR code.
*
* @param options optional configuration for the output.
* @returns a string suitable for use as the `src` of an `img` tag.
*/
toDataURL(
options?: Readonly<
ImageDataOptions & {
type?: `image/${string}`;
scale?: number;
}
>
): string;
/**
* Populate a given canvas with the QR code, at a scale of 1 pixel per
* module. Set image-rendering: pixelated and scale the canvas using CSS
* for a large image. Automatically resizes the canvas to fit the QR code
* if necessary.
*
* @param canvas the canvas to populate.
* @param options optional configuration for the display.
*/
toCanvas(
canvas: CanvasLike<ImageDataLike>,
options?: Readonly<ImageDataOptions>
): void;
}
export type Mask = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
export type Mode = (data: Bitmap1D, version: number) => void;
export interface ModeFactory {
(value: string): Mode;
/** a function which returns true when given a character which the current mode can represent */
test(string: string): boolean;
/** a function which returns an estimate of the number of bits required to encode a given value */
est(value: string, version: number): number;
/** an optional ECI which must be active for this mode to be interpreted correctly by a reader */
eci?: number;
}
interface ModeAutoOptions {
/** a list of modes which can be considered when encoding a message */
modes?: ReadonlyArray<ModeFactory>;
}
export const mode: Readonly<{
/** automatically picks the most optimal combination of modes for the requested message */
auto(value: string, options?: Readonly<ModeAutoOptions>): Mode;
/** concatenates multiple modes together */
multi(...modes: ReadonlyArray<Mode>): Mode;
/** sets the Extended Channel Interpretation for the message from this point onwards */
eci(id: number): Mode;
/** supports `0-9` and stores 3 characters per 10 bits */
numeric: ModeFactory;
/** supports `0-9A-Z $%*+-./:` and stores 2 characters per 11 bits */
alphaNumeric: ModeFactory;
/** arbitrary byte data, typically combined with `eci` */
bytes(data: Uint8Array | ReadonlyArray<number>): Mode;
/** supports 7-bit ASCII and stores 1 character per 8 bits with no ECI */
ascii: ModeFactory;
/** supports 8-bit ISO-8859-1 and stores 1 character per 8 bits with ECI 3 */
iso8859_1: ModeFactory;
/** supports double-byte Shift-JIS characters stores 1 character per 13 bits */
shift_jis: ModeFactory;
/** supports variable length UTF-8 with ECI 26 */
utf8: ModeFactory;
}>;
export type Correction = number & { readonly _: unique symbol };
export const correction: Readonly<{
/** minimum possible correction level (same as L) */
min: Correction;
/** ~7.5% error tolerance, ~25% data overhead */
L: Correction;
/** ~15% error tolerance, ~60% data overhead */
M: Correction;
/** ~22.5% error tolerance, ~120% data overhead */
Q: Correction;
/** ~30% error tolerance, ~190% data overhead */
H: Correction;
/** maximum possible correction level (same as H) */
max: Correction;
}>;
export interface GenerateOptions extends ModeAutoOptions {
/** the minimum correction level to use (higher levels may still be used if the chosen version has space) */
minCorrectionLevel?: Correction;
/** the maximum correction level to use */
maxCorrectionLevel?: Correction;
/** the minimum version (size) of code to generate (must be between 1 and 40) */
minVersion?: number;
/** the maximum version (size) of code to generate (must be between 1 and 40) */
maxVersion?: number;
/** a mask to use on the QR code (should be left as `null` for ISO compliance but may be changed for artistic effect) */
mask?: null | Mask;
/** padding bits to use for extra space in the QR code (should be left as the default for ISO compliance but may be changed for artistic effect) */
trailer?: number;
}
/**
* Generate a QR code.
*
* @param data either a string, or a pre-encoded mode.
* @param options optional configuration for the QR code.
* @returns the requested QR code.
*/
export type GenerateFn = (
data: Mode | string,
options?: Readonly<GenerateOptions>
) => Bitmap2D;
interface Generate extends GenerateFn {
/**
* Creates a scoped `generate` function which considers additional modes
* when using auto encoding.
*
* @param modes the modes to add.
* @returns a `generate` function which will additionally consider the
* given modes when using auto encoding.
*/
with(...modes: ReadonlyArray<ModeFactory>): GenerateFn;
}
export const generate: Generate;
}
declare module "lean-qr/nano" {
import type {
Correction,
Bitmap2D as FullBitmap2D,
GenerateOptions as FullGenerateOptions,
} from "lean-qr";
import { correction as fullCorrection } from "lean-qr";
export type { Correction };
export const correction: Pick<typeof fullCorrection, "L" | "M" | "Q" | "H">;
export type Bitmap2D = Pick<FullBitmap2D, "size" | "get" | "toCanvas">;
export type GenerateOptions = Pick<
FullGenerateOptions,
"minCorrectionLevel" | "minVersion"
>;
/**
* Generate a QR code.
*
* @param data either a string, or a pre-encoded mode.
* @param options optional configuration for the QR code.
* @returns the requested QR code.
*/
export function generate(
data: string,
options?: Readonly<GenerateOptions>
): Bitmap2D;
}
declare module "lean-qr/extras/svg" {
import type { Bitmap2D as FullBitmap2D } from "lean-qr";
type Bitmap2D = Pick<FullBitmap2D, "size" | "get">;
export interface SVGOptions {
/** the colour to use for modules which are 'on' (typically black) */
on?: string;
/** the colour to use for modules which are 'off' (typically white) */
off?: string;
/** the padding to apply on the left and right of the output (filled with 'off') */
padX?: number;
/** the padding to apply on the top and bottom of the output (filled with 'off') */
padY?: number;
/** a width to apply to the resulting image (overrides `scale`) */
width?: number | null;
/** a height to apply to the resulting image (overrides `scale`) */
height?: number | null;
/** a scale to apply to the resulting image (`scale` pixels = 1 module) */
scale?: number;
}
/**
* Generate the raw outline of the QR code for use in an existing SVG.
*
* @param code the QR code to convert.
* @returns a string suitable for passing to the `d` attribute of a `path`.
*/
export function toSvgPath(code: Bitmap2D): string;
/**
* Generate an SVG element which can be added to the DOM.
*
* @param code the QR code to convert.
* @param options optional configuration for the display.
* @returns an SVG element.
*/
export function toSvg(
code: Bitmap2D,
target: Document | SVGElement,
options?: Readonly<SVGOptions>
): SVGElement;
/**
* Generate an SVG document which can be exported to a file or served from a
* web server.
*
* @param code the QR code to convert.
* @param options optional configuration for the display.
* @returns an SVG document.
*/
export function toSvgSource(
code: Bitmap2D,
options?: Readonly<
SVGOptions & {
/** `true` to include an XML declaration at the start of the source (for standalone documents which will not be embedded inside another document) */
xmlDeclaration?: boolean;
}
>
): string;
/**
* Generate a `data:image/svg+xml` URL.
*
* @param code the QR code to convert.
* @param options optional configuration for the display.
* @returns a string suitable for use as the `src` of an `img` tag.
*/
export function toSvgDataURL(
code: Bitmap2D,
options?: Readonly<SVGOptions>
): string;
}
declare module "lean-qr/extras/node_export" {
import type { RGBA, Bitmap2D as FullBitmap2D } from "lean-qr";
type Bitmap2D = Pick<FullBitmap2D, "size" | "get">;
export interface PNGOptions {
/** the colour to use for modules which are 'on' (typically black) */
on?: RGBA;
/** the colour to use for modules which are 'off' (typically white) */
off?: RGBA;
/** the padding to apply on the left and right of the output (filled with 'off') */
padX?: number;
/** the padding to apply on the top and bottom of the output (filled with 'off') */
padY?: number;
/** a scale to apply to the resulting image (`scale` pixels = 1 module) */
scale?: number;
}
/**
* Generate a PNG document which can be exported to a file or served from a
* web server.
*
* @param code the QR code to convert.
* @param options optional configuration for the display.
* @returns a PNG document.
*/
export function toPngBuffer(
code: Bitmap2D,
options?: Readonly<PNGOptions>
): Uint8Array;
/**
* Generate a `data:image/png` URL.
*
* @param code the QR code to convert.
* @param options optional configuration for the display.
* @returns a string suitable for use as the `src` of an `img` tag.
*/
export function toPngDataURL(
code: Bitmap2D,
options?: Readonly<PNGOptions>
): string;
}
declare module "lean-qr/extras/react" {
import type {
Bitmap2D as FullBitmap2D,
GenerateOptions,
ImageDataOptions,
} from "lean-qr";
import type {
SVGOptions,
toSvgDataURL as toSvgDataURLFn,
} from "lean-qr/extras/svg";
export interface AsyncFramework<T> {
createElement: (
type: "canvas",
props: {
ref: any;
style: { imageRendering: "pixelated" };
className: string;
}
) => T;
useRef<T>(initialValue: T | null): { readonly current: T | null };
useEffect(fn: () => void | (() => void), deps: unknown[]): void;
}
interface QRComponentProps {
content: string;
className?: string;
}
export interface AsyncQRComponentProps
extends ImageDataOptions,
GenerateOptions,
QRComponentProps {}
export type AsyncQRComponent<T> = (
props: Readonly<AsyncQRComponentProps>
) => T;
/**
* Generate an asynchronous QR component (rendering to a `canvas`).
* You should call this just once, in the global scope.
*
* This is not suitable for server-side rendering (use `makeSyncComponent`
* instead).
*
* @param framework the framework to use (e.g. `React`).
* @param generate the `generate` function to use
* (from `lean-qr` or `lean-qr/nano`).
* @param defaultProps optional default properties to apply when the
* component is used (overridden by properties set on use).
* @returns a component which can be rendered elsewhere.
*/
export function makeAsyncComponent<T>(
framework: Readonly<AsyncFramework<T>>,
generate: (
data: string,
options?: Readonly<GenerateOptions>
) => Pick<FullBitmap2D, "toCanvas">,
defaultProps?: Readonly<Partial<AsyncQRComponentProps>>
): AsyncQRComponent<T>;
export interface SyncFramework<T> {
createElement: (
type: "img",
props: {
src: string;
style: { imageRendering: "pixelated" };
className: string;
}
) => T;
useMemo<T>(fn: () => T, deps: unknown[]): T;
}
export interface SyncQRComponentProps
extends SVGOptions,
GenerateOptions,
QRComponentProps {}
export type SyncQRComponent<T> = (props: Readonly<SyncQRComponentProps>) => T;
/**
* Generate a synchronous QR component (rendering to an SVG).
* You should call this just once, in the global scope.
*
* This is best suited for server-side rendering (prefer
* `makeAsyncComponent` if you only need client-side rendering).
*
* @param framework the framework to use (e.g. `React`).
* @param generate the `generate` function to use
* (from `lean-qr` or `lean-qr/nano`).
* @param toSvgDataURL the `toSvgDataURL` function to use
* (from `lean-qr/extras/svg`).
* @param defaultProps optional default properties to apply when the
* component is used (overridden by properties set on use).
* @returns a component which can be rendered elsewhere.
*/
export function makeSyncComponent<T>(
framework: Readonly<SyncFramework<T>>,
generate: (
data: string,
options?: Readonly<GenerateOptions>
) => Pick<FullBitmap2D, "size" | "get">,
toSvgDataURL: typeof toSvgDataURLFn,
defaultProps?: Readonly<Partial<SyncQRComponentProps>>
): SyncQRComponent<T>;
}
declare module "lean-qr/extras/errors" {
/**
* Convert an error into a human-readable message. This is intended for use
* with Lean QR errors, but will return somewhat meaningful messages for
* other errors too.
*
* @param error the error to convert.
* @returns a human-readable message explaining the error.
*/
export function readError(error: unknown): string;
}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
import { Accessor, Setter } from "./v0.3.0-treeshaked/types/signals";
import { Accessor, Setter } from "./v0.3.2-treeshaked/types/signals";
export type Signal<T> = Accessor<T> & { set: Setter<T>; reset: VoidFunction };
export type Signals = Awaited<typeof import("./wrapper.js").default>;
@@ -1,9 +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, getObserver, isEqual, untrack, hasUpdated, isPending, latest, flatten, UNCHANGED, compute, runWithObserver, type SignalOptions } from "./core.js";
export { Effect, EagerComputation } from "./effect.js";
export { flushSync, type IQueue, Queue } from "./scheduler.js";
export { createSuspense, createErrorBoundary, createBoundary } from "./boundaries.js";
export { SUPPORTS_PROXY } from "./constants.js";
export { tryCatch, type TryCatchResult } from "./utils.js";
export * from "./flags.js";
@@ -1,4 +0,0 @@
export declare function isUndefined(value: any): value is undefined;
export type TryCatchResult<T, E> = [undefined, T] | [E];
export declare function tryCatch<T, E = Error>(fn: () => Promise<T>): Promise<TryCatchResult<T, E>>;
export declare function tryCatch<T, E = Error>(fn: () => T): TryCatchResult<T, E>;
@@ -1,3 +0,0 @@
export { Owner, getOwner, onCleanup, untrack } from "./core/index.js";
export type { ErrorHandler, SignalOptions, Context, ContextRecord, Disposable, IQueue } from "./core/index.js";
export * from "./signals.js";
@@ -1,7 +1,7 @@
// @ts-nocheck
// src/core/error.ts
var NotReadyError = class extends Error {
};
var NotReadyError = class extends Error {};
var EffectError = class extends Error {
constructor(effect, cause) {
super("");
@@ -28,69 +28,59 @@ function incrementClock() {
}
var scheduled = false;
function schedule() {
if (scheduled)
return;
if (scheduled) return;
scheduled = true;
if (!globalQueue.y)
queueMicrotask(flushSync);
if (!globalQueue.u) queueMicrotask(flushSync);
}
var pureQueue = [];
var Queue = class {
i = null;
y = false;
m = [[], [], []];
v = [];
u = false;
v = [[], []];
t = [];
created = clock;
enqueue(type, node) {
this.m[0].push(node);
if (type)
this.m[type].push(node);
enqueue(type, fn) {
pureQueue.push(fn);
if (type) this.v[type - 1].push(fn);
schedule();
}
run(type) {
if (this.m[type].length) {
if (type === EFFECT_PURE) {
runPureQueue(this.m[type]);
this.m[type] = [];
} else {
const effects = this.m[type];
this.m[type] = [];
runEffectQueue(effects);
}
if (type === EFFECT_PURE) {
pureQueue.length && runQueue(pureQueue, type);
pureQueue = [];
return;
} else if (this.v[type - 1].length) {
const effects = this.v[type - 1];
this.v[type - 1] = [];
runQueue(effects, type);
}
let rerun = false;
for (let i = 0; i < this.v.length; i++) {
rerun = this.v[i].run(type) || rerun;
for (let i = 0; i < this.t.length; i++) {
this.t[i].run(type);
}
if (type === EFFECT_PURE)
return rerun || !!this.m[type].length;
}
flush() {
if (this.y)
return;
this.y = true;
if (this.u) return;
this.u = true;
try {
while (this.run(EFFECT_PURE)) {
}
this.run(EFFECT_PURE);
incrementClock();
scheduled = false;
this.run(EFFECT_RENDER);
this.run(EFFECT_USER);
} finally {
this.y = false;
this.u = false;
}
}
addChild(child) {
this.v.push(child);
this.t.push(child);
child.i = this;
}
removeChild(child) {
const index = this.v.indexOf(child);
if (index >= 0)
this.v.splice(index, 1);
const index = this.t.indexOf(child);
if (index >= 0) this.t.splice(index, 1);
}
notify(...args) {
if (this.i)
return this.i.notify(...args);
if (this.i) return this.i.notify(...args);
return false;
}
};
@@ -100,27 +90,8 @@ function flushSync() {
globalQueue.flush();
}
}
function runTop(node) {
const ancestors = [];
for (let current = node; current !== null; current = current.i) {
if (current.a !== STATE_CLEAN) {
ancestors.push(current);
}
}
for (let i = ancestors.length - 1; i >= 0; i--) {
if (ancestors[i].a !== STATE_DISPOSED)
ancestors[i].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].L();
function runQueue(queue, type) {
for (let i = 0; i < queue.length; i++) queue[i](type);
}
// src/core/owner.ts
@@ -134,106 +105,96 @@ function setOwner(owner) {
currentOwner = owner;
return out;
}
function formatId(prefix, id) {
const num = id.toString(36), len = num.length - 1;
return prefix + (len ? String.fromCharCode(64 + len) : "") + num;
}
var Owner = class {
// We flatten the owner tree into a linked list so that we don't need a pointer to .firstChild
// However, the children are actually added in reverse creation order
// See comment at the top of the file for an example of the _nextSibling traversal
i = null;
g = null;
n = null;
a = STATE_CLEAN;
h = null;
l = null;
a = STATE_CLEAN;
f = null;
j = defaultContext;
f = globalQueue;
G = null;
M = 0;
g = globalQueue;
F = 0;
id = null;
constructor(id = null, skipAppend = false) {
this.id = id;
if (currentOwner && !skipAppend)
currentOwner.append(this);
if (currentOwner) {
!skipAppend && currentOwner.append(this);
}
}
append(child) {
child.i = this;
child.n = this;
if (this.id) {
child.G = this.g ? this.g.G + 1 : 0;
child.id = formatId(this.id, child.G);
}
if (this.g)
this.g.n = child;
child.g = this.g;
this.g = child;
child.l = this;
if (this.h) this.h.l = child;
child.h = this.h;
this.h = child;
if (this.id != null && child.id == null) child.id = this.getNextChildId();
if (child.j !== this.j) {
child.j = { ...this.j, ...child.j };
}
if (this.f)
child.f = this.f;
if (this.g) child.g = this.g;
}
dispose(self = true) {
if (this.a === STATE_DISPOSED)
return;
let head = self ? this.n || this.i : this, current = this.g, next = null;
if (this.a === STATE_DISPOSED) return;
let head = self ? this.l || this.i : this,
current = this.h,
next = null;
while (current && current.i === this) {
current.dispose(true);
current.q();
next = current.g;
current.g = null;
current.o();
next = current.h;
current.h = null;
current = next;
}
this.M = 0;
if (self)
this.q();
if (current)
current.n = !self ? this : this.n;
if (head)
head.g = current;
this.F = 0;
if (self) this.o();
if (current) current.l = !self ? this : this.l;
if (head) head.h = current;
}
q() {
if (this.n)
this.n.g = null;
o() {
if (this.l) this.l.h = null;
this.i = null;
this.n = null;
this.l = null;
this.j = defaultContext;
this.a = STATE_DISPOSED;
this.emptyDisposal();
}
emptyDisposal() {
if (!this.h)
return;
if (Array.isArray(this.h)) {
for (let i = 0; i < this.h.length; i++) {
const callable = this.h[i];
if (!this.f) return;
if (Array.isArray(this.f)) {
for (let i = 0; i < this.f.length; i++) {
const callable = this.f[i];
callable.call(callable);
}
} else {
this.h.call(this.h);
this.f.call(this.f);
}
this.h = null;
this.f = null;
}
getNextChildId() {
if (this.id)
return formatId(this.id + "-", this.M++);
if (this.id != null) return formatId(this.id, this.F++);
throw new Error("Cannot get child id from owner without an id");
}
};
function onCleanup(fn) {
if (!currentOwner)
return fn;
if (!currentOwner) return fn;
const node = currentOwner;
if (!node.h) {
node.h = fn;
} else if (Array.isArray(node.h)) {
node.h.push(fn);
if (!node.f) {
node.f = fn;
} else if (Array.isArray(node.f)) {
node.f.push(fn);
} else {
node.h = [node.h, fn];
node.f = [node.f, fn];
}
return fn;
}
function formatId(prefix, id) {
const num = id.toString(36),
len = num.length - 1;
return prefix + (len ? String.fromCharCode(64 + len) : "") + num;
}
// src/core/flags.ts
var ERROR_OFFSET = 0;
@@ -257,36 +218,32 @@ var Computation = class extends Owner {
c = null;
e;
w;
r;
p;
// Used in __DEV__ mode, hopefully removed in production
Q;
J;
// Using false is an optimization as an alternative to _equals: () => false
// which could enable more efficient DIRTY notification
H = isEqual;
N;
A = isEqual;
G;
/** Whether the computation is an error or has ancestors that are unresolved */
d = 0;
/** Which flags raised by sources are handled, vs. being passed through. */
I = DEFAULT_FLAGS;
s = -1;
z = false;
B = DEFAULT_FLAGS;
q = -1;
n = false;
constructor(initialValue, compute2, options) {
super(null, compute2 === null);
this.r = compute2;
super(options?.id, compute2 === null);
this.p = compute2;
this.a = compute2 ? STATE_DIRTY : STATE_CLEAN;
this.d = compute2 && initialValue === void 0 ? UNINITIALIZED_BIT : 0;
this.e = initialValue;
if (options?.equals !== void 0)
this.H = options.equals;
if (options?.unobserved)
this.N = options?.unobserved;
if (options?.equals !== void 0) this.A = options.equals;
if (options?.unobserved) this.G = options?.unobserved;
}
O() {
if (this.r) {
if (this.d & ERROR_BIT && this.s <= getClock())
update(this);
else
this.p();
H() {
if (this.p) {
if (this.d & ERROR_BIT && this.q <= getClock()) update(this);
else this.r();
}
track(this);
newFlags |= this.d & ~currentMask;
@@ -301,7 +258,7 @@ var Computation = class extends Owner {
* Automatically re-executes the surrounding computation when the value changes
*/
read() {
return this.O();
return this.H();
}
/**
* Return the current value of this computation
@@ -311,34 +268,41 @@ var Computation = class extends Owner {
* before continuing
*/
wait() {
if (this.r && this.d & ERROR_BIT && this.s <= getClock()) {
if (this.p && this.d & ERROR_BIT && this.q <= getClock()) {
update(this);
} else {
this.p();
this.r();
}
track(this);
if ((notStale || this.d & UNINITIALIZED_BIT) && this.d & LOADING_BIT) {
throw new NotReadyError();
}
return this.O();
return this.H();
}
/** Update the computation with a new value. */
write(value, flags = 0, raw = false) {
const newValue = !raw && typeof value === "function" ? value(this.e) : value;
const valueChanged = newValue !== UNCHANGED && (!!(this.d & UNINITIALIZED_BIT) || this.d & LOADING_BIT & ~flags || this.H === false || !this.H(this.e, newValue));
const newValue =
!raw && typeof value === "function" ? value(this.e) : value;
const valueChanged =
newValue !== UNCHANGED &&
(!!(this.d & UNINITIALIZED_BIT) ||
this.d & LOADING_BIT & ~flags ||
this.A === false ||
!this.A(this.e, newValue));
if (valueChanged) {
this.e = newValue;
this.w = void 0;
}
const changedFlagsMask = this.d ^ flags, changedFlags = changedFlagsMask & flags;
const changedFlagsMask = this.d ^ flags,
changedFlags = changedFlagsMask & flags;
this.d = flags;
this.s = getClock() + 1;
this.q = getClock() + 1;
if (this.c) {
for (let i = 0; i < this.c.length; i++) {
if (valueChanged) {
this.c[i].l(STATE_DIRTY);
this.c[i].k(STATE_DIRTY);
} else if (changedFlagsMask) {
this.c[i].P(changedFlagsMask, changedFlags);
this.c[i].I(changedFlagsMask, changedFlags);
}
}
}
@@ -347,14 +311,13 @@ var Computation = class extends Owner {
/**
* Set the current node's state, and recursively mark all of this node's observers as STATE_CHECK
*/
l(state, skipQueue) {
if (this.a >= state && !this.z)
return;
this.z = !!skipQueue;
k(state, skipQueue) {
if (this.a >= state && !this.n) return;
this.n = !!skipQueue;
this.a = state;
if (this.c) {
for (let i = 0; i < this.c.length; i++) {
this.c[i].l(STATE_CHECK, skipQueue);
this.c[i].k(STATE_CHECK, skipQueue);
}
}
}
@@ -364,31 +327,33 @@ var Computation = class extends Owner {
* @param mask A bitmask for which flag(s) were changed.
* @param newFlags The source's new flags, masked to just the changed ones.
*/
P(mask, newFlags2) {
if (this.a >= STATE_DIRTY)
return;
if (mask & this.I) {
this.l(STATE_DIRTY);
I(mask, newFlags2) {
if (this.a >= STATE_DIRTY) return;
if (mask & this.B) {
this.k(STATE_DIRTY);
return;
}
if (this.a >= STATE_CHECK)
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.l(STATE_CHECK);
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].P(mask, newFlags2);
this.c[i].I(mask, newFlags2);
}
}
}
}
J(error) {
C(error) {
this.w = error;
this.write(UNCHANGED, this.d & ~LOADING_BIT | ERROR_BIT | UNINITIALIZED_BIT);
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
@@ -397,8 +362,8 @@ var Computation = class extends Owner {
*
* This function will ensure that the value and states we read from the computation are up to date
*/
p() {
if (!this.r) {
r() {
if (!this.p) {
return;
}
if (this.a === STATE_DISPOSED) {
@@ -410,7 +375,7 @@ var Computation = class extends Owner {
let observerFlags = 0;
if (this.a === STATE_CHECK) {
for (let i = 0; i < this.b.length; i++) {
this.b[i].p();
this.b[i].r();
observerFlags |= this.b[i].d;
if (this.a === STATE_DIRTY) {
break;
@@ -427,45 +392,50 @@ var Computation = class extends Owner {
/**
* 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();
o() {
if (this.a === STATE_DISPOSED) return;
if (this.b) removeSourceObservers(this, 0);
super.o();
}
};
function track(computation) {
if (currentObserver) {
if (!newSources && currentObserver.b && currentObserver.b[newSourcesIndex] === computation) {
if (
!newSources &&
currentObserver.b &&
currentObserver.b[newSourcesIndex] === computation
) {
newSourcesIndex++;
} else if (!newSources)
newSources = [computation];
} 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;
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);
const result = compute(node, node.p, node);
node.write(result, newFlags, true);
} catch (error) {
if (error instanceof NotReadyError) {
node.write(UNCHANGED, newFlags | LOADING_BIT | node.d & UNINITIALIZED_BIT);
node.write(
UNCHANGED,
newFlags | LOADING_BIT | (node.d & UNINITIALIZED_BIT)
);
} else {
node.J(error);
node.C(error);
}
} finally {
if (newSources) {
if (node.b)
removeSourceObservers(node, newSourcesIndex);
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++) {
@@ -477,10 +447,8 @@ function update(node) {
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);
if (!source.c) source.c = [node];
else source.c.push(node);
}
} else if (node.b && newSourcesIndex < node.b.length) {
removeSourceObservers(node, newSourcesIndex);
@@ -489,7 +457,7 @@ function update(node) {
newSources = prevSources;
newSourcesIndex = prevSourcesIndex;
newFlags = prevFlags;
node.s = getClock() + 1;
node.q = getClock() + 1;
node.a = STATE_CLEAN;
}
}
@@ -502,8 +470,7 @@ function removeSourceObservers(node, index) {
swap = source.c.indexOf(node);
source.c[swap] = source.c[source.c.length - 1];
source.c.pop();
if (!source.c.length)
source.N?.();
if (!source.c.length) source.G?.();
}
}
}
@@ -511,8 +478,7 @@ function isEqual(a, b) {
return a === b;
}
function untrack(fn) {
if (currentObserver === null)
return fn();
if (currentObserver === null) return fn();
return compute(getOwner(), fn, null);
}
function latest(fn, fallback) {
@@ -523,8 +489,7 @@ function latest(fn, fallback) {
try {
return fn();
} catch (err) {
if (argLength > 1 && err instanceof NotReadyError)
return fallback;
if (argLength > 1 && err instanceof NotReadyError) return fallback;
throw err;
} finally {
newFlags = prevFlags;
@@ -532,9 +497,12 @@ function latest(fn, fallback) {
}
}
function compute(owner, fn, observer) {
const prevOwner = setOwner(owner), prevObserver = currentObserver, prevMask = currentMask, prevNotStale = notStale;
const prevOwner = setOwner(owner),
prevObserver = currentObserver,
prevMask = currentMask,
prevNotStale = notStale;
currentObserver = observer;
currentMask = observer?.I ?? DEFAULT_FLAGS;
currentMask = observer?.B ?? DEFAULT_FLAGS;
notStale = true;
try {
return fn(observer ? observer.e : void 0);
@@ -548,85 +516,100 @@ function compute(owner, fn, observer) {
// src/core/effect.ts
var Effect = class extends Computation {
A;
B;
t;
K = false;
C;
o;
x;
y;
s;
D = false;
z;
m;
constructor(initialValue, compute2, effect, error, options) {
super(initialValue, compute2, options);
this.A = effect;
this.B = error;
this.C = 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.x = effect;
this.y = error;
this.z = initialValue;
this.m = options?.render ? EFFECT_RENDER : EFFECT_USER;
if (this.m === EFFECT_RENDER) {
this.p = (p) =>
getClock() > this.g.created && !(this.d & ERROR_BIT)
? latest(() => compute2(p))
: compute2(p);
}
this.p();
!options?.defer && (this.o === EFFECT_USER ? this.f.enqueue(this.o, this) : this.L());
this.r();
!options?.defer &&
(this.m === EFFECT_USER
? this.g.enqueue(this.m, this.E.bind(this))
: this.E(this.m));
}
write(value, flags = 0) {
if (this.a == STATE_DIRTY) {
this.d;
this.d = flags;
if (this.o === EFFECT_RENDER) {
this.f.notify(this, LOADING_BIT | ERROR_BIT, flags);
if (this.m === EFFECT_RENDER) {
this.g.notify(this, LOADING_BIT | ERROR_BIT, flags);
}
}
if (value === UNCHANGED)
return this.e;
if (value === UNCHANGED) return this.e;
this.e = value;
this.K = true;
this.D = true;
return value;
}
l(state, skipQueue) {
if (this.a >= state || skipQueue)
return;
if (this.a === STATE_CLEAN)
this.f.enqueue(this.o, this);
k(state, skipQueue) {
if (this.a >= state || skipQueue) return;
if (this.a === STATE_CLEAN) this.g.enqueue(this.m, this.E.bind(this));
this.a = state;
}
J(error) {
C(error) {
this.w = error;
this.t?.();
this.f.notify(this, LOADING_BIT, 0);
this.s?.();
this.g.notify(this, LOADING_BIT, 0);
this.d = ERROR_BIT;
if (this.o === EFFECT_USER) {
if (this.m === EFFECT_USER) {
try {
return this.B ? this.t = this.B(error) : console.error(new EffectError(this.A, error));
return this.y
? (this.s = this.y(error))
: console.error(new EffectError(this.x, error));
} catch (e) {
error = e;
}
}
if (!this.f.notify(this, ERROR_BIT, ERROR_BIT))
throw error;
if (!this.g.notify(this, ERROR_BIT, ERROR_BIT)) throw error;
}
q() {
if (this.a === STATE_DISPOSED)
return;
this.A = void 0;
this.C = void 0;
this.B = void 0;
this.t?.();
this.t = void 0;
super.q();
o() {
if (this.a === STATE_DISPOSED) return;
this.x = void 0;
this.z = void 0;
this.y = void 0;
this.s?.();
this.s = void 0;
super.o();
}
L() {
if (this.K && this.a !== STATE_DISPOSED) {
this.t?.();
try {
this.t = this.A(this.e, this.C);
} catch (e) {
if (!this.f.notify(this, ERROR_BIT, ERROR_BIT))
throw e;
} finally {
this.C = this.e;
this.K = false;
E(type) {
if (type) {
if (this.D && this.a !== STATE_DISPOSED) {
this.s?.();
try {
this.s = this.x(this.e, this.z);
} catch (e) {
if (!this.g.notify(this, ERROR_BIT, ERROR_BIT)) throw e;
} finally {
this.z = this.e;
this.D = false;
}
}
}
} else this.a !== STATE_CLEAN && runTop(this);
}
};
function runTop(node) {
const ancestors = [];
for (let current = node; current !== null; current = current.i) {
if (current.a !== STATE_CLEAN) {
ancestors.push(current);
}
}
for (let i = ancestors.length - 1; i >= 0; i--) {
if (ancestors[i].a !== STATE_DISPOSED) ancestors[i].r();
}
}
// src/signals.ts
function createSignal(first, second, third) {
@@ -641,15 +624,17 @@ function createSignal(first, second, third) {
});
return [() => memo()[0](), (value) => memo()[1](value)];
}
const node = new Computation(first, null, second);
const o = getOwner();
const needsId = o?.id != null;
const node = new Computation(
first,
null,
needsId ? { id: o.getNextChildId(), ...second } : second
);
return [node.read.bind(node), node.write.bind(node)];
}
function createMemo(compute2, value, options) {
let node = new Computation(
value,
compute2,
options
);
let node = new Computation(value, compute2, options);
let resolvedValue;
return () => {
if (node) {
@@ -658,7 +643,7 @@ function createMemo(compute2, value, options) {
return resolvedValue;
}
resolvedValue = node.wait();
if (!node.b?.length && node.g?.i !== node) {
if (!node.b?.length && node.h?.i !== node) {
node.dispose();
node = void 0;
}
@@ -667,20 +652,28 @@ function createMemo(compute2, value, options) {
};
}
function createEffect(compute2, effect, error, value, options) {
void new Effect(
value,
compute2,
effect,
error,
options
);
void new Effect(value, compute2, effect, error, options);
}
function createRoot(init, options) {
const owner = new Owner(options?.id);
return compute(owner, !init.length ? init : () => init(() => owner.dispose()), null);
return compute(
owner,
!init.length ? init : () => init(() => owner.dispose()),
null
);
}
function runWithOwner(owner, run) {
return compute(owner, run, null);
}
export { Owner, createEffect, createMemo, createRoot, createSignal, getOwner, onCleanup, runWithOwner, untrack };
export {
Owner,
createEffect,
createMemo,
createRoot,
createSignal,
getOwner,
onCleanup,
runWithOwner,
untrack,
};
@@ -1,11 +1,11 @@
import { Computation } from "./core.js";
import { type Effect } from "./effect.js";
import { Queue } from "./scheduler.js";
import { Computation, Queue } from "./core/index.js";
import type { Effect } from "./core/index.js";
export declare class CollectionQueue extends Queue {
_collectionType: number;
_nodes: Set<Effect>;
_disabled: Computation<boolean>;
constructor(type: number);
run(type: number): void;
notify(node: Effect, type: number, flags: number): boolean;
}
export declare enum BoundaryMode {
@@ -15,3 +15,7 @@ export declare enum BoundaryMode {
export declare function createBoundary<T>(fn: () => T, condition: () => BoundaryMode): () => T | undefined;
export declare function createSuspense(fn: () => any, fallback: () => any): () => any;
export declare function createErrorBoundary<U>(fn: () => any, fallback: (error: unknown, reset: () => void) => U): () => any;
export declare function flatten(children: any, options?: {
skipNonRendered?: boolean;
doNotUnwrap?: boolean;
}): any;
@@ -29,6 +29,7 @@
import { type Flags } from "./flags.js";
import { Owner } from "./owner.js";
export interface SignalOptions<T> {
id?: string;
name?: string;
equals?: ((prev: T, next: T) => boolean) | false;
unobserved?: () => void;
@@ -130,7 +131,7 @@ export declare function untrack<T>(fn: () => T): T;
*/
export declare function hasUpdated(fn: () => any): boolean;
/**
* Returns an accessor that is true if the given function contains async signals are out of date.
* Returns an accessor that is true if the given function contains async signals that are out of date.
*/
export declare function isPending(fn: () => any): boolean;
export declare function isPending(fn: () => any, loadingValue: boolean): boolean;
@@ -151,8 +152,4 @@ export declare function runWithObserver<T>(observer: Computation, run: () => T):
*/
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 {};
@@ -20,7 +20,7 @@ export declare class Effect<T = any> extends Computation<T> {
_notify(state: number, skipQueue?: boolean): void;
_setError(error: unknown): void;
_disposeNode(): void;
_runEffect(): void;
_runEffect(type: number): void;
}
export declare class EagerComputation<T = any> extends Computation<T> {
constructor(initialValue: T, compute: () => T, options?: SignalOptions<T> & {
@@ -1,4 +1,3 @@
import type { Owner } from "./owner.js";
export declare class NotReadyError extends Error {
}
export declare class NoOwnerError extends Error {
@@ -10,6 +9,3 @@ export declare class ContextNotFoundError extends Error {
export declare class EffectError extends Error {
constructor(effect: Function, cause: unknown);
}
export interface ErrorHandler {
(error: unknown, node: Owner): void;
}
@@ -0,0 +1,7 @@
export { ContextNotFoundError, NoOwnerError, NotReadyError } from "./error.js";
export { Owner, createContext, getContext, setContext, hasContext, getOwner, onCleanup, type Context, type ContextRecord, type Disposable } from "./owner.js";
export { Computation, getObserver, isEqual, untrack, hasUpdated, isPending, latest, UNCHANGED, compute, runWithObserver, type SignalOptions } from "./core.js";
export { Effect, EagerComputation } from "./effect.js";
export { flushSync, Queue, incrementClock, getClock, type IQueue } from "./scheduler.js";
export * from "./constants.js";
export * from "./flags.js";
@@ -45,7 +45,6 @@ export declare class Owner {
_disposal: Disposable | Disposable[] | null;
_context: ContextRecord;
_queue: IQueue;
_siblingCount: number | null;
_childCount: number;
id: string | null;
constructor(id?: string | null, skipAppend?: boolean);
@@ -1,9 +1,8 @@
import type { Computation } from "./core.js";
import type { Effect } from "./effect.js";
export declare function getClock(): number;
export declare function incrementClock(): void;
type QueueCallback = (type: number) => void;
export interface IQueue {
enqueue<T extends Computation | Effect>(type: number, node: T): void;
enqueue(type: number, fn: QueueCallback): void;
run(type: number): boolean | void;
flush(): void;
addChild(child: IQueue): void;
@@ -15,11 +14,11 @@ export interface IQueue {
export declare class Queue implements IQueue {
_parent: IQueue | null;
_running: boolean;
_queues: [Computation[], Effect[], Effect[]];
_queues: [QueueCallback[], QueueCallback[]];
_children: IQueue[];
created: number;
enqueue<T extends Computation | Effect>(type: number, node: T): void;
run(type: number): boolean | undefined;
enqueue(type: number, fn: QueueCallback): void;
run(type: number): void;
flush(): void;
addChild(child: IQueue): void;
removeChild(child: IQueue): void;
@@ -31,3 +30,4 @@ export declare const globalQueue: Queue;
* the queue synchronously to get the latest updates by calling `flushSync()`.
*/
export declare function flushSync(): void;
export {};
@@ -0,0 +1,3 @@
export { Owner, getOwner, onCleanup, untrack } from "./core/index.js";
export type { SignalOptions, Context, ContextRecord, Disposable, IQueue } from "./core/index.js";
export * from "./signals.js";
@@ -62,6 +62,21 @@ export declare function createSignal<T>(fn: ComputeFunction<T>, initialValue?: T
*/
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 readonly derived async reactive memoized signal
* ```typescript
* export function createAsync<T>(
* compute: (v: T) => Promise<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-async
*/
/**
* Creates a reactive effect that runs after the render phase
* ```typescript
@@ -82,6 +97,23 @@ export declare function createMemo<Next extends Prev, Init = Next, Prev = Next>(
*/
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 reactive computation that runs during the render phase as DOM elements are created and updated but not necessarily connected
* ```typescript
* export function createRenderEffect<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
* @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/secondary-primitives/create-render-effect
*/
/**
* Creates a new non-tracked reactive context with manual disposal
*
@@ -100,3 +132,7 @@ export declare function createRoot<T>(init: ((dispose: () => void) => T) | (() =
* 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
*/
@@ -1,13 +1,15 @@
// @ts-check
/**
* @import { SignalOptions } from "./v0.3.0-treeshaked/types/core/core"
* @import { getOwner as GetOwner, onCleanup as OnCleanup, Owner } from "./v0.3.0-treeshaked/types/core/owner"
* @import { createSignal as CreateSignal, createEffect as CreateEffect, Accessor, Setter, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner } from "./v0.3.0-treeshaked/types/signals";
* @import { SignalOptions } from "./v0.3.2-treeshaked/types/core/core"
* @import { getOwner as GetOwner, onCleanup as OnCleanup } from "./v0.3.2-treeshaked/types/core/owner"
* @import { createSignal as CreateSignal, createEffect as CreateEffect, createMemo as CreateMemo, createRoot as CreateRoot, runWithOwner as RunWithOwner, Accessor } from "./v0.3.2-treeshaked/types/signals";
* @import { Signal } from "./types";
*/
const importSignals = import("./v0.3.0-treeshaked/script.js").then(
let effectCount = 0;
const importSignals = import("./v0.3.2-treeshaked/script.js").then(
(_signals) => {
const signals = {
createSolidSignal: /** @type {typeof CreateSignal} */ (
@@ -20,16 +22,30 @@ const importSignals = import("./v0.3.0-treeshaked/script.js").then(
// @ts-ignore
(compute, effect) => {
let dispose = /** @type {VoidFunction | null} */ (null);
if (_signals.getOwner() === null) {
throw Error("No owner");
}
function cleanup() {
if (dispose) {
dispose();
dispose = null;
console.debug("effectCount = ", --effectCount);
}
}
// @ts-ignore
_signals.createEffect(compute, (v, oldV) => {
dispose?.();
console.debug("effectCount = ", ++effectCount);
cleanup();
signals.createRoot((_dispose) => {
dispose = _dispose;
return effect(v, oldV);
});
signals.onCleanup(() => dispose?.());
signals.onCleanup(cleanup);
});
signals.onCleanup(() => dispose?.());
signals.onCleanup(cleanup);
}
),
createMemo: /** @type {typeof CreateMemo} */ (_signals.createMemo),
@@ -40,7 +56,7 @@ const importSignals = import("./v0.3.0-treeshaked/script.js").then(
/**
* @template T
* @param {T} initialValue
* @param {SignalOptions<T> & {save?: {keyPrefix: string; key: string; serialize: (v: T) => string; deserialize: (v: string) => T; serializeParam?: boolean}}} [options]
* @param {SignalOptions<T> & {save?: {keyPrefix: string | Accessor<string>; key: string; serialize: (v: T) => string; deserialize: (v: string) => T; serializeParam?: boolean}}} [options]
* @returns {Signal<T>}
*/
createSignal(initialValue, options) {
@@ -59,7 +75,10 @@ const importSignals = import("./v0.3.0-treeshaked/script.js").then(
const save = options.save;
const paramKey = save.key;
const storageKey = `${save.keyPrefix}-${paramKey}`;
const storageKey = this.createMemo(
() =>
`${typeof save.keyPrefix === "string" ? save.keyPrefix : save.keyPrefix()}-${paramKey}`,
);
let serialized = /** @type {string | null} */ (null);
if (options.save.serializeParam !== false) {
@@ -67,19 +86,31 @@ const importSignals = import("./v0.3.0-treeshaked/script.js").then(
paramKey,
);
}
if (serialized === null) {
serialized = localStorage.getItem(storageKey);
serialized = localStorage.getItem(storageKey());
}
if (serialized) {
set(() => save.deserialize(serialized));
set(() =>
serialized ? save.deserialize(serialized) : initialValue,
);
}
let firstEffect = true;
let firstRun1 = true;
this.createEffect(storageKey, (storageKey) => {
if (!firstRun1) {
serialized = localStorage.getItem(storageKey);
set(() =>
serialized ? save.deserialize(serialized) : initialValue,
);
}
firstRun1 = false;
});
let firstRun2 = true;
this.createEffect(get, (value) => {
if (!save) return;
if (!firstEffect) {
if (!firstRun2) {
if (
value !== undefined &&
value !== null &&
@@ -87,9 +118,9 @@ const importSignals = import("./v0.3.0-treeshaked/script.js").then(
initialValue === null ||
save.serialize(value) !== save.serialize(initialValue))
) {
localStorage.setItem(storageKey, save.serialize(value));
localStorage.setItem(storageKey(), save.serialize(value));
} else {
localStorage.removeItem(storageKey);
localStorage.removeItem(storageKey());
}
}
@@ -105,7 +136,7 @@ const importSignals = import("./v0.3.0-treeshaked/script.js").then(
removeParam(paramKey);
}
firstEffect = false;
firstRun2 = false;
});
}
@@ -131,11 +162,13 @@ function writeParam(key, value) {
urlParams.delete(key);
}
window.history.replaceState(
null,
"",
`${window.location.pathname}?${urlParams.toString()}`,
);
try {
window.history.replaceState(
null,
"",
`${window.location.pathname}?${urlParams.toString()}`,
);
} catch (_) {}
}
/**
+1 -1
View File
@@ -5,4 +5,4 @@ Head:
- SHA: 6bb27a8d8c41e4be5458844afc5c89f6c2399512
- Date: Feb 21, 2024
- Version: v1.0.14
- Version: v1.0.18
@@ -1,16 +1,15 @@
// @ts-nocheck
/**
* Copyright (c) 2024, Leon Sorokin
* Copyright (c) 2025, Leon Sorokin
* All rights reserved. (MIT Licensed)
*
* uFuzzy.js (μFuzzy)
* A tiny, efficient fuzzy matcher that doesn't suck
* https://github.com/leeoniya/uFuzzy (v1.0.14)
* https://github.com/leeoniya/uFuzzy (v1.0.18)
*/
const cmp = new Intl.Collator("en", { numeric: true, sensitivity: "base" })
.compare;
const cmp = (a, b) => (a > b ? 1 : a < b ? -1 : 0);
const inf = Infinity;
@@ -25,6 +24,8 @@ const PUNCT_RE = /\p{P}/gu;
const LATIN_UPPER = "A-Z";
const LATIN_LOWER = "a-z";
const COLLATE_ARGS = ["en", { numeric: true, sensitivity: "base" }];
const swapAlpha = (str, upper, lower) =>
str.replace(LATIN_UPPER, upper).replace(LATIN_LOWER, lower);
@@ -75,8 +76,12 @@ const OPTS = {
// (since intraIns is between each char, it can accum to nonsense matches)
intraFilt: (term, match, index) => true, // should this also accept WIP info?
toUpper: (str) => str.toLocaleUpperCase(),
toLower: (str) => str.toLocaleLowerCase(),
compare: null,
// final sorting fn
sort: (info, haystack, needle) => {
sort: (info, haystack, needle, compare = cmp) => {
let {
idx,
chars,
@@ -88,6 +93,7 @@ const OPTS = {
start,
intraIns,
interIns,
cases,
} = info;
return idx
@@ -109,8 +115,10 @@ const OPTS = {
interIns[ia] - interIns[ib] ||
// earliest start of match
start[ia] - start[ib] ||
// case match
cases[ib] - cases[ia] ||
// alphabetic
cmp(haystack[idx[ia]], haystack[idx[ib]])
compare(haystack[idx[ia]], haystack[idx[ib]])
);
},
};
@@ -145,6 +153,9 @@ function uFuzzy(opts) {
intraBound: _intraBound,
interBound: _interBound,
intraChars,
toUpper,
toLower,
compare,
} = opts;
intraIns ??= intraMode;
@@ -152,11 +163,16 @@ function uFuzzy(opts) {
intraTrn ??= intraMode;
intraDel ??= intraMode;
compare ??=
typeof Intl == "undefined"
? cmp
: new Intl.Collator(...COLLATE_ARGS).compare;
let alpha = opts.letters ?? opts.alpha;
if (alpha != null) {
let upper = alpha.toLocaleUpperCase();
let lower = alpha.toLocaleLowerCase();
let upper = toUpper(alpha);
let lower = toLower(alpha);
_interSplit = swapAlpha(_interSplit, upper, lower);
_intraSplit = swapAlpha(_intraSplit, upper, lower);
@@ -232,7 +248,7 @@ function uFuzzy(opts) {
);
let contrsRe = new RegExp(intraContr, "gi" + uFlag);
const split = (needle) => {
const split = (needle, keepCase = false) => {
let exacts = [];
needle = needle.replace(EXACTS_RE, (m) => {
@@ -240,7 +256,9 @@ function uFuzzy(opts) {
return EXACT_HERE;
});
needle = needle.replace(trimRe, "").toLocaleLowerCase();
needle = needle.replace(trimRe, "");
if (!keepCase) needle = toLower(needle);
if (withIntraSplit)
needle = needle.replace(intraSplit, (m) => m[0] + " " + m[1]);
@@ -460,9 +478,25 @@ function uFuzzy(opts) {
const info = (idxs, haystack, needle) => {
let [query, parts, contrs] = prepQuery(needle, 1);
let partsCased = split(needle, true);
let [queryR] = prepQuery(needle, 2);
let partsLen = parts.length;
let _terms = Array(partsLen);
let _termsCased = Array(partsLen);
for (let j = 0; j < partsLen; j++) {
let part = parts[j];
let partCased = partsCased[j];
let term = part[0] == '"' ? part.slice(1, -1) : part + contrs[j];
let termCased =
partCased[0] == '"' ? partCased.slice(1, -1) : partCased + contrs[j];
_terms[j] = term;
_termsCased[j] = termCased;
}
let len = idxs.length;
let field = Array(len).fill(0);
@@ -479,6 +513,9 @@ function uFuzzy(opts) {
// contiguous chars matched
chars: field.slice(),
// case matched in term (via term.includes(match))
cases: field.slice(),
// contiguous (no fuzz) and bounded terms (intra=0, lft2/1, rgt2/1)
// excludes terms that are contiguous but have < 2 bounds (substrings)
terms: field.slice(),
@@ -520,24 +557,27 @@ function uFuzzy(opts) {
let rgt1 = 0;
let chars = 0;
let terms = 0;
let cases = 0;
let inter = 0;
let intra = 0;
let refine = [];
for (let j = 0, k = 2; j < partsLen; j++, k += 2) {
let group = m[k].toLocaleLowerCase();
let part = parts[j];
let term = part[0] == '"' ? part.slice(1, -1) : part + contrs[j];
let group = toLower(m[k]);
let term = _terms[j];
let termCased = _termsCased[j];
let termLen = term.length;
let groupLen = group.length;
let fullMatch = group == term;
if (m[k] == termCased) cases++;
// this won't handle the case when an exact match exists across the boundary of the current group and the next junk
// e.g. blob,ob when searching for 'bob' but finding the earlier `blob` (with extra insertion)
if (!fullMatch && m[k + 1].length >= termLen) {
// probe for exact match in inter junk (TODO: maybe even in this matched part?)
let idxOf = m[k + 1].toLocaleLowerCase().indexOf(term);
let idxOf = toLower(m[k + 1]).indexOf(term);
if (idxOf > -1) {
refine.push(idxAcc, groupLen, idxOf, termLen);
@@ -685,6 +725,7 @@ function uFuzzy(opts) {
info.interRgt1[ii] = rgt1;
info.chars[ii] = chars;
info.terms[ii] = terms;
info.cases[ii] = cases;
info.interIns[ii] = inter;
info.intraIns[ii] = intra;
@@ -906,7 +947,7 @@ function uFuzzy(opts) {
let needle = needles[ni];
let _info = info(idxs, haystack, needle);
let order = opts.sort(_info, haystack, needle);
let order = opts.sort(_info, haystack, needle, compare);
// offset idxs for concat'ing infos
if (ni > 0) {
@@ -34,7 +34,7 @@ declare class uFuzzy {
): uFuzzy.InfoIdxOrder;
/** utility for splitting needle into terms following defined interSplit/intraSplit opts. useful for out-of-order permutes */
split(needle: string): uFuzzy.Terms;
split(needle: string, keepCase?: boolean): uFuzzy.Terms;
/** util for creating out-of-order permutations of a needle terms array */
static permute(arr: unknown[]): unknown[][];
@@ -99,6 +99,8 @@ declare namespace uFuzzy {
export type IntraSliceIdxs = [from: number, to: number];
type CompareFn = (a: string, b: string) => number;
export interface Options {
// whether regexps use a /u unicode flag
unicode?: boolean; // false
@@ -158,7 +160,21 @@ declare namespace uFuzzy {
/** post-filters matches during .info() based on cmp of term in needle vs partial match */
intraFilt?: (term: string, match: string, index: number) => boolean; // should this also accept WIP info?
sort?: (info: Info, haystack: string[], needle: string) => InfoIdxOrder;
/** default: toLocaleUpperCase() */
toUpper?: (str: string) => string;
/** default: toLocaleLowerCase() */
toLower?: (str: string) => string;
/** final sorting cmp when all other match metrics are equal */
compare?: CompareFn;
sort?: (
info: Info,
haystack: string[],
needle: string,
compare?: CompareFn
) => InfoIdxOrder;
}
export interface Info {
@@ -188,6 +204,9 @@ declare namespace uFuzzy {
/** number of exactly-matched terms (intra = 0) where both lft and rgt landed on a BoundMode.Loose or BoundMode.Strict boundary */
terms: number[];
/** number of needle terms with case-sensitive partial matches */
cases: number[];
/** offset ranges within match for highlighting: [startIdx0, endIdx0, startIdx1, endIdx1,...] */
ranges: number[][];
}
+258 -214
View File
@@ -1,5 +1,7 @@
// @ts-check
const keyPrefix = "chart";
/**
* @param {Object} args
* @param {Colors} args.colors
@@ -26,10 +28,36 @@ export function init({
elements.charts.append(utils.dom.createShadow("left"));
elements.charts.append(utils.dom.createShadow("right"));
const { headerElement, headingElement } = utils.dom.createHeader({});
const { headerElement, headingElement } = utils.dom.createHeader();
elements.charts.append(headerElement);
const { index, fieldset } = createIndexSelector({ signals, utils });
const TIMERANGE_LS_KEY = signals.createMemo(
() => `chart-timerange-${index()}`,
);
let firstRun = true;
const from = signals.createSignal(/** @type {number | null} */ (null), {
save: {
...utils.serde.optNumber,
keyPrefix: TIMERANGE_LS_KEY,
key: "from",
serializeParam: firstRun,
},
});
const to = signals.createSignal(/** @type {number | null} */ (null), {
save: {
...utils.serde.optNumber,
keyPrefix: TIMERANGE_LS_KEY,
key: "to",
serializeParam: firstRun,
},
});
const chart = lightweightCharts.createChartElement({
owner: signals.getOwner(),
parent: elements.charts,
signals,
colors,
@@ -37,201 +65,229 @@ export function init({
utils,
vecsResources,
elements,
index,
timeScaleSetCallback: (unknownTimeScaleCallback) => {
// TODO: Although it mostly works in practice, need to make it more robust, there is no guarantee that this runs in order and wait for `from` and `to` to update when `index` and thus `TIMERANGE_LS_KEY` is updated
// Need to have the right values before the update
const from_ = from();
const to_ = to();
if (from_ !== null && to_ !== null) {
chart.inner.timeScale().setVisibleLogicalRange({
from: from_,
to: to_,
});
} else {
unknownTimeScaleCallback();
}
},
});
const index = createIndexSelector({ elements, signals, utils });
chart.inner.timeScale().subscribeVisibleLogicalRangeChange(
utils.debounce((t) => {
if (t) {
from.set(t.from);
to.set(t.to);
}
}),
);
let firstRun = true;
elements.charts.append(fieldset);
const { field: seriesTypeField, selected: topSeriesType } =
utils.dom.createHorizontalChoiceField({
defaultValue: "Line",
keyPrefix,
key: "seriestype-0",
choices: /** @type {const} */ (["Candles", "Line"]),
signals,
});
const { field: topUnitField, selected: topUnit } =
utils.dom.createHorizontalChoiceField({
defaultValue: "USD",
keyPrefix,
key: "unit-0",
choices: /** @type {const} */ ([
/** @satisfies {Unit} */ ("USD"),
/** @satisfies {Unit} */ ("Sats"),
]),
signals,
sorted: true,
});
chart.addFieldsetIfNeeded({
id: "charts-unit-0",
paneIndex: 0,
position: "nw",
createChild() {
return topUnitField;
},
});
const seriesListTop = /** @type {Series[]} */ ([]);
const seriesListBottom = /** @type {Series[]} */ ([]);
signals.createEffect(selected, (option) => {
headingElement.innerHTML = option.title;
const bottomUnits = /** @type {readonly Unit[]} */ (
Object.keys(option.bottom)
);
const { field: bottomUnitField, selected: bottomUnit } =
utils.dom.createHorizontalChoiceField({
defaultValue: bottomUnits.at(0) || "",
keyPrefix,
key: "unit-1",
choices: bottomUnits,
signals,
sorted: true,
});
if (bottomUnits.length) {
chart.addFieldsetIfNeeded({
id: "charts-unit-1",
paneIndex: 1,
position: "nw",
createChild() {
return bottomUnitField;
},
});
}
chart.addFieldsetIfNeeded({
id: "charts-seriestype-0",
paneIndex: 0,
position: "ne",
createChild() {
return seriesTypeField;
},
});
signals.createEffect(index, (index) => {
const { field: topUnitField, selected: topUnit } =
utils.dom.createHorizontalChoiceField({
defaultValue: "USD",
keyPrefix: "charts",
key: "unit-0",
choices: /** @type {const} */ ([
/** @satisfies {Unit} */ ("USD"),
/** @satisfies {Unit} */ ("Sats"),
]),
signals,
sorted: true,
});
signals.createEffect(topUnit, (topUnit) => {
const { field: seriesTypeField, selected: topSeriesType } =
utils.dom.createHorizontalChoiceField({
defaultValue: "Line",
keyPrefix: "charts",
key: "seriestype-0",
choices: /** @type {const} */ (["Candles", "Line"]),
signals,
});
signals.createEffect(topSeriesType, (topSeriesType) => {
const bottomUnits = /** @type {readonly Unit[]} */ (
Object.keys(option.bottom)
);
const { field: bottomUnitField, selected: bottomUnit } =
utils.dom.createHorizontalChoiceField({
defaultValue: bottomUnits.at(0) || "",
keyPrefix: "charts",
key: "unit-1",
choices: bottomUnits,
signals,
sorted: true,
});
signals.createEffect(bottomUnit, (bottomUnit) => {
chart.reset({ owner: signals.getOwner() });
chart.addFieldsetIfNeeded({
id: "charts-unit-0",
paneIndex: 0,
position: "nw",
createChild() {
return topUnitField;
},
});
if (bottomUnits.length) {
chart.addFieldsetIfNeeded({
id: "charts-unit-1",
paneIndex: 1,
position: "nw",
createChild() {
return bottomUnitField;
},
});
}
chart.addFieldsetIfNeeded({
id: "charts-seriestype-0",
paneIndex: 0,
position: "ne",
createChild() {
return seriesTypeField;
},
});
const TIMERANGE_LS_KEY = `chart-timerange-${index}`;
const from = signals.createSignal(
/** @type {number | null} */ (null),
{
save: {
...utils.serde.optNumber,
keyPrefix: TIMERANGE_LS_KEY,
key: "from",
serializeParam: firstRun,
},
},
);
const to = signals.createSignal(
/** @type {number | null} */ (null),
{
save: {
...utils.serde.optNumber,
keyPrefix: TIMERANGE_LS_KEY,
key: "to",
serializeParam: firstRun,
},
},
);
chart.create({
index,
timeScaleSetCallback: (unknownTimeScaleCallback) => {
const from_ = from();
const to_ = to();
if (from_ !== null && to_ !== null) {
chart.inner()?.timeScale().setVisibleLogicalRange({
from: from_,
to: to_,
signals.createEffect(
() => [topUnit(), topSeriesType()],
([topUnit, topSeriesType]) => {
switch (topUnit) {
case "USD": {
switch (topSeriesType) {
case "Candles": {
const series = chart.addCandlestickSeries({
vecId: "ohlc",
name: "Price",
unit: topUnit,
order: 0,
});
} else {
unknownTimeScaleCallback();
seriesListTop[0]?.remove();
seriesListTop[0] = series;
break;
}
},
case "Line": {
const series = chart.addLineSeries({
vecId: "close",
name: "Price",
unit: topUnit,
color: colors.default,
options: {
priceLineVisible: true,
},
order: 0,
});
seriesListTop[0]?.remove();
seriesListTop[0] = series;
}
}
// signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
// if (!latest) return;
// const last = /** @type { CandlestickData | undefined} */ (
// candles.data().at(-1)
// );
// if (!last) return;
// candles?.update({ ...last, close: latest.close });
// });
break;
}
case "Sats": {
switch (topSeriesType) {
case "Candles": {
const series = chart.addCandlestickSeries({
vecId: "ohlc-in-sats",
name: "Price",
unit: topUnit,
inverse: true,
order: 0,
});
seriesListTop[0]?.remove();
seriesListTop[0] = series;
break;
}
case "Line": {
const series = chart.addLineSeries({
vecId: "close-in-sats",
name: "Price",
unit: topUnit,
color: colors.default,
options: {
priceLineVisible: true,
},
order: 0,
});
seriesListTop[0]?.remove();
seriesListTop[0] = series;
}
}
break;
}
}
},
);
[
{
blueprints: option.top,
paneIndex: 0,
unit: topUnit,
seriesList: seriesListTop,
orderStart: 1,
legend: chart.legendTop,
},
{
blueprints: option.bottom,
paneIndex: 1,
unit: bottomUnit,
seriesList: seriesListBottom,
orderStart: 0,
legend: chart.legendBottom,
},
].forEach(
({
blueprints,
paneIndex,
unit,
seriesList: seriesList,
orderStart,
legend,
}) => {
signals.createEffect(unit, (unit) => {
legend.removeFrom(orderStart);
seriesList.splice(orderStart).forEach((series) => {
series.remove();
});
switch (topUnit) {
case "USD": {
switch (topSeriesType) {
case "Candles": {
const candles = chart.addCandlestickSeries({
vecId: "ohlc",
name: "Price",
unit: topUnit,
});
break;
}
case "Line": {
const line = chart.addLineSeries({
vecId: "close",
name: "Price",
unit: topUnit,
color: colors.default,
options: {
priceLineVisible: true,
},
});
}
}
// signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
// if (!latest) return;
// const last = /** @type { CandlestickData | undefined} */ (
// candles.data().at(-1)
// );
// if (!last) return;
// candles?.update({ ...last, close: latest.close });
// });
break;
}
case "Sats": {
switch (topSeriesType) {
case "Candles": {
const candles = chart.addCandlestickSeries({
vecId: "ohlc-in-sats",
name: "Price",
unit: topUnit,
inverse: true,
});
break;
}
case "Line": {
const line = chart.addLineSeries({
vecId: "close-in-sats",
name: "Price",
unit: topUnit,
color: colors.default,
options: {
priceLineVisible: true,
},
});
}
}
break;
}
}
blueprints[unit]?.forEach((blueprint, order) => {
order += orderStart;
[
{ blueprints: option.top, paneIndex: 0 },
{ blueprints: option.bottom, paneIndex: 1 },
].forEach(({ blueprints, paneIndex }) => {
const unit = paneIndex ? bottomUnit : topUnit;
const indexes = /** @type {readonly number[]} */ (
vecIdToIndexes[blueprint.key]
);
blueprints[unit]?.forEach((blueprint) => {
const indexes = /** @type {readonly number[]} */ (
vecIdToIndexes[blueprint.key]
);
if (indexes.includes(index)) {
switch (blueprint.type) {
case "Baseline": {
if (indexes.includes(index)) {
switch (blueprint.type) {
case "Baseline": {
seriesList.push(
chart.addBaselineSeries({
vecId: blueprint.key,
// color: blueprint.color,
name: blueprint.title,
unit,
defaultActive: blueprint.defaultActive,
@@ -243,14 +299,16 @@ export function init({
bottomLineColor:
blueprint.color?.() ?? blueprint.colors?.[1](),
},
});
break;
}
case "Candlestick": {
throw Error("TODO");
break;
}
default:
order,
}),
);
break;
}
case "Candlestick": {
throw Error("TODO");
}
default:
seriesList.push(
chart.addLineSeries({
vecId: blueprint.key,
color: blueprint.color,
@@ -259,43 +317,30 @@ export function init({
defaultActive: blueprint.defaultActive,
paneIndex,
options: blueprint.options,
});
}
order,
}),
);
}
});
}
});
chart
.inner()
?.timeScale()
.subscribeVisibleLogicalRangeChange(
utils.debounce((t) => {
if (t) {
from.set(t.from);
to.set(t.to);
}
}),
);
firstRun = false;
});
});
});
},
);
firstRun = false;
});
});
}
/**
* @param {Object} args
* @param {Elements} args.elements
* @param {Signals} args.signals
* @param {Utilities} args.utils
*/
function createIndexSelector({ elements, signals, utils }) {
function createIndexSelector({ signals, utils }) {
const { field, selected } = utils.dom.createHorizontalChoiceField({
// title: "Index",
defaultValue: "date",
keyPrefix: "charts",
keyPrefix,
key: "index",
choices: /**@type {const} */ ([
"timestamp",
@@ -315,7 +360,6 @@ function createIndexSelector({ elements, signals, utils }) {
const fieldset = window.document.createElement("fieldset");
fieldset.append(field);
fieldset.dataset.size = "sm";
elements.charts.append(fieldset);
const index = signals.createMemo(
/** @returns {ChartableIndex} */ () => {
@@ -338,5 +382,5 @@ function createIndexSelector({ elements, signals, utils }) {
},
);
return index;
return { fieldset, index };
}
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -2795,10 +2795,6 @@ function createPartialOptions(colors) {
{
name: "Social",
tree: [
{
name: "Github",
url: () => "https://github.com/bitcoinresearchkit/brk",
},
{
name: "Nostr",
url: () =>
@@ -2867,6 +2863,10 @@ function createPartialOptions(colors) {
url: () =>
"lightning:lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhkxmmww3jkuar8d35kgetj8yuq363hv4",
},
{
name: "Geyser",
url: () => "https://geyser.fund/project/brk",
},
],
},
{
@@ -1,76 +0,0 @@
const version = "v1";
/** @type {ServiceWorkerGlobalScope} */
const sw = /** @type {any} */ (self);
sw.addEventListener("install", (_event) => {
console.log("sw: install");
sw.skipWaiting();
});
sw.addEventListener("activate", (event) => {
console.log("sw: active");
event.waitUntil(sw.clients.claim());
});
sw.addEventListener("fetch", (event) => {
let request = event.request;
const method = request.method;
let url = request.url;
const { pathname, origin } = new URL(url);
const slashMatches = url.match(/\//g);
const dotMatches = pathname.split("/").at(-1)?.match(/./g);
const endsWithDotHtml = pathname.endsWith(".html");
const slashApiSlashMatches = url.match(/\/api\//g);
if (
slashMatches &&
slashMatches.length <= 3 &&
!slashApiSlashMatches &&
(!dotMatches || endsWithDotHtml)
) {
url = `${origin}/`;
}
request = new Request(url, request.mode !== "navigate" ? request : undefined);
console.log(request);
console.log(`service-worker: fetch ${url}`);
event.respondWith(
caches.match(request).then(async (cachedResponse) => {
return fetch(request)
.then((response) => {
const { status, type } = response;
if (method !== "GET" || slashApiSlashMatches) {
// API calls are cached in script.js
return response;
} else if ((status === 200 || status === 304) && type === "basic") {
if (status === 200) {
const clonedResponse = response.clone();
caches.open(version).then((cache) => {
cache.put(request, clonedResponse);
});
}
return response;
} else {
return cachedResponse || response;
}
})
.catch(() => {
console.log("service-worker: offline");
return (
cachedResponse ||
new Response("Offline", {
status: 503,
statusText: "Service Unavailable",
})
);
});
}),
);
});
+17 -9
View File
@@ -391,9 +391,7 @@ export function init({
};
parametersElement.append(
utils.dom.createHeader({
title: "Save in Bitcoin",
}).headerElement,
utils.dom.createHeader("Save in Bitcoin").headerElement,
);
/**
@@ -698,12 +696,16 @@ export function init({
},
);
const index = () => /** @type {DateIndex} */ (0);
lightweightCharts.createChartElement({
index,
owner,
parent: resultsElement,
signals,
colors,
id: `result`,
fitContentOnResize: true,
fitContent: true,
vecsResources,
utils,
elements,
@@ -743,11 +745,13 @@ export function init({
});
lightweightCharts.createChartElement({
index,
owner,
parent: resultsElement,
signals,
colors,
id: `bitcoin`,
fitContentOnResize: true,
fitContent: true,
vecsResources,
elements,
utils,
@@ -767,11 +771,13 @@ export function init({
});
lightweightCharts.createChartElement({
index,
owner,
parent: resultsElement,
signals,
colors,
id: `average-price`,
fitContentOnResize: true,
fitContent: true,
vecsResources,
utils,
elements,
@@ -797,15 +803,16 @@ export function init({
});
lightweightCharts.createChartElement({
index,
owner,
parent: resultsElement,
signals,
colors,
vecsResources,
id: `return-ratio`,
fitContentOnResize: true,
fitContent: true,
utils,
elements,
config: [
{
unit: "USD",
@@ -821,11 +828,12 @@ export function init({
});
lightweightCharts.createChartElement({
index,
parent: resultsElement,
signals,
colors,
id: `simulation-profitability-ratios`,
fitContentOnResize: true,
fitContent: true,
vecsResources,
utils,
elements,
+5 -3
View File
@@ -196,6 +196,10 @@ function createTable({
signal: vecIdOption,
});
signals.createEffect(vecIdOption, (vecIdOption) => {
select.style.width = `${21 + 7.25 * vecIdOption.name.length}px`;
});
if (_colIndex === columns().length) {
columns.set((l) => {
l.push(vecId);
@@ -364,9 +368,7 @@ export function init({
vecIdToIndexes,
}) {
const parent = elements.table;
const { headerElement } = utils.dom.createHeader({
title: "Table",
});
const { headerElement } = utils.dom.createHeader("Table");
parent.append(headerElement);
const div = window.document.createElement("div");
+2 -2
View File
@@ -2,6 +2,8 @@
// File auto-generated, any modifications will be overwritten
//
export const VERSION = "v0.0.48";
/** @typedef {0} DateIndex */
/** @typedef {1} DecadeIndex */
/** @typedef {2} DifficultyEpoch */
@@ -30,8 +32,6 @@
/** @typedef {DateIndex | DecadeIndex | DifficultyEpoch | EmptyOutputIndex | HalvingEpoch | Height | InputIndex | MonthIndex | OpReturnIndex | OutputIndex | P2AIndex | P2MSIndex | P2PK33Index | P2PK65Index | P2PKHIndex | P2SHIndex | P2TRIndex | P2WPKHIndex | P2WSHIndex | QuarterIndex | TxIndex | UnknownOutputIndex | WeekIndex | YearIndex} Index */
export function createVecIdToIndexes() {
return /** @type {const} */ ({
"0": [0, 1, 2, 5, 7, 19, 22, 23],
"0sats-adjusted-spent-output-profit-ratio": [0],
+89
View File
@@ -0,0 +1,89 @@
const CACHE_NAME = "cache";
/** @type {ServiceWorkerGlobalScope} */
const sw = /** @type {any} */ (self);
sw.addEventListener("install", (event) => {
console.log("sw: install");
event.waitUntil(sw.skipWaiting());
});
sw.addEventListener("activate", (event) => {
console.log("sw: active");
event.waitUntil(sw.clients.claim());
event.waitUntil(
caches
.keys()
.then((keys) =>
Promise.all(
keys.filter((key) => key !== "api").map((key) => caches.delete(key)),
),
),
);
});
async function indexHTMLOrOffline() {
return caches.match("/index.html").then((cached) => {
if (cached) return cached;
return new Response("Offline and no cached version", {
status: 503,
statusText: "Service Unavailable",
headers: { "Content-Type": "text/plain" },
});
});
}
sw.addEventListener("fetch", (event) => {
const req = event.request;
const url = new URL(req.url);
// 1) Bypass API calls & non-GETs
if (req.method !== "GET" || url.pathname.startsWith("/api")) {
return; // let the browser handle it
}
// 2) NAVIGATION: networkfirst on your shell
if (req.mode === "navigate") {
event.respondWith(
// Always fetch index.html
fetch("/index.html")
.then((response) => {
// If we got a valid 2xx back, cache it (optional) and return it
if (response.ok || response.status === 304) {
if (response.ok) {
const clone = response.clone();
caches
.open(CACHE_NAME)
.then((cache) => cache.put("/index.html", clone));
}
return response;
}
throw new Error("Non-2xx on shell");
})
// On any failure, fall back to the cached shell
.catch(indexHTMLOrOffline),
);
return;
}
// 3) For all other GETs: network-first, fallback to cache
event.respondWith(
fetch(req)
.then((response) => {
if (response.ok) {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(req, clone));
}
return response;
})
.catch(async () => {
return caches
.match(req)
.then((cached) => {
return cached || indexHTMLOrOffline();
})
.catch(indexHTMLOrOffline);
})
.catch(indexHTMLOrOffline),
);
});
+5 -4
View File
@@ -28,11 +28,12 @@
flex: 1;
}
.lightweight-chart {
z-index: 30;
> .chart > legend,
> fieldset {
z-index: 20;
}
> * {
/* z-index: 30; */
.lightweight-chart {
z-index: 40;
}
}
+2 -3
View File
@@ -68,9 +68,8 @@
.lightweight-chart {
margin-left: calc(var(--negative-main-padding) * 0.75);
fieldset {
margin-left: -0.5rem;
}
fieldset {
margin-left: -0.5rem;
}
}
}
+9 -7
View File
@@ -7,7 +7,9 @@
> div {
display: flex;
font-size: var(--font-size-sm);
font-size: var(--font-size-xs);
line--line-height: var(--line-height-xs);
font-weight: 450;
margin-left: var(--negative-main-padding);
margin-right: var(--negative-main-padding);
@@ -36,7 +38,7 @@
border-bottom: 1px;
border-color: var(--off-color);
border-style: dashed !important;
padding: 0.25rem 1rem;
padding: 0.25rem 0.75rem;
}
td {
@@ -84,7 +86,7 @@
> button {
padding: 0 0.25rem;
margin: 0 -0.25rem;
font-size: 1rem;
font-size: 0.75rem;
line-height: 0;
}
}
@@ -109,9 +111,10 @@
}
}
/* select {
width: 100%;
} */
select {
margin-right: -4px;
/* width: 100%; */
}
tbody {
text-align: right;
@@ -126,7 +129,6 @@
position: relative;
border-top-width: 1px;
width: 100%;
/* border-right-width: 1px; */
border-bottom-width: 1px;
border-style: dashed !important;
+2 -1
View File
@@ -5,7 +5,8 @@
"target": "ESNext",
"module": "ESNext",
"outDir": "/tmp/brk",
"lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"]
"lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"],
"skipLibCheck": true
},
"exclude": ["assets", "packages", "ignore"]
}