mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-29 19:58:10 -07:00
global: snapshot
This commit is contained in:
Generated
+12
-12
@@ -1954,9 +1954,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "importmap"
|
name = "importmap"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d13d6361899f14d58146b6214c07e63cda8270c3ef3b8c30626f8e20e6766eb"
|
checksum = "a45c4c5e8b4d0a6aed90f3a14125bd3af3bca73d49d44e8f8b764311903b5213"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rapidhash",
|
"rapidhash",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2684,7 +2684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"rand_core 0.9.3",
|
"rand_core 0.9.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2704,7 +2704,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core 0.9.3",
|
"rand_core 0.9.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2718,9 +2718,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.9.3"
|
version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
checksum = "4f1b3bc831f92381018fd9c6350b917c7b21f1eed35a65a51900e0e55a3d7afa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
@@ -2745,9 +2745,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rawdb"
|
name = "rawdb"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1eb09ba02f9467845fde4a1fadb317721025f2b836f22a5a7d3567c9e100875"
|
checksum = "eb8999fc8f80521c13091f98e1a24bde2448b29c0637f008dc470ed84d58f703"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
@@ -3689,9 +3689,9 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vecdb"
|
name = "vecdb"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b7b02690e7c013257b959b482fac78e90f73764efa8a57551e1e67a06ad7ab4"
|
checksum = "1a31985bee527adda1601eab9c72de0a567be83a08e80e646b79afb77ab14970"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"log",
|
"log",
|
||||||
@@ -3710,9 +3710,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vecdb_derive"
|
name = "vecdb_derive"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21339c58345d1a422c2574b1114b3cd862900a9196421dc7acf43aca48c288bb"
|
checksum = "61716cbeb322dcc9838cea11b533cea278f9592b2ef2ce520595849833cfee1c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
|
|||||||
+1
-1
@@ -79,7 +79,7 @@ serde_json = { version = "1.0.149", features = ["float_roundtrip", "preserve_ord
|
|||||||
smallvec = "1.15.1"
|
smallvec = "1.15.1"
|
||||||
tokio = { version = "1.49.0", features = ["rt-multi-thread"] }
|
tokio = { version = "1.49.0", features = ["rt-multi-thread"] }
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
vecdb = { version = "0.5.8", features = ["derive", "serde_json", "pco", "schemars"] }
|
vecdb = { version = "0.5.9", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||||
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
|
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||||
|
|
||||||
[workspace.metadata.release]
|
[workspace.metadata.release]
|
||||||
|
|||||||
@@ -458,4 +458,33 @@ mod tests {
|
|||||||
Some(&FieldNamePosition::Append("_cumulative".to_string()))
|
Some(&FieldNamePosition::Append("_cumulative".to_string()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_analyze_pattern_level_no_base_field() {
|
||||||
|
// When there's no base field (like block_weight which has no block_weight metric),
|
||||||
|
// only suffixed metrics like block_weight_average, block_weight_sum, etc.
|
||||||
|
// Base should still be "block_weight"
|
||||||
|
let children = vec![
|
||||||
|
("average".to_string(), "block_weight_average".to_string()),
|
||||||
|
("sum".to_string(), "block_weight_sum".to_string()),
|
||||||
|
(
|
||||||
|
"cumulative".to_string(),
|
||||||
|
"block_weight_cumulative".to_string(),
|
||||||
|
),
|
||||||
|
("max".to_string(), "block_weight_max".to_string()),
|
||||||
|
("min".to_string(), "block_weight_min".to_string()),
|
||||||
|
];
|
||||||
|
let analysis = analyze_pattern_level(&children);
|
||||||
|
|
||||||
|
assert!(matches!(analysis.common, CommonDenominator::Prefix(_)));
|
||||||
|
assert_eq!(analysis.base, "block_weight");
|
||||||
|
assert_eq!(
|
||||||
|
analysis.field_positions.get("average"),
|
||||||
|
Some(&FieldNamePosition::Append("_average".to_string()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
analysis.field_positions.get("sum"),
|
||||||
|
Some(&FieldNamePosition::Append("_sum".to_string()))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
|
|||||||
|
|
||||||
use brk_types::{Index, TreeNode, extract_json_type};
|
use brk_types::{Index, TreeNode, extract_json_type};
|
||||||
|
|
||||||
use crate::{IndexSetPattern, PatternField, analysis::names::analyze_pattern_level, child_type_name};
|
use crate::{IndexSetPattern, PatternField, analysis::names::{analyze_pattern_level, CommonDenominator}, child_type_name};
|
||||||
|
|
||||||
/// Get the first leaf name from a tree node.
|
/// Get the first leaf name from a tree node.
|
||||||
pub fn get_first_leaf_name(node: &TreeNode) -> Option<String> {
|
pub fn get_first_leaf_name(node: &TreeNode) -> Option<String> {
|
||||||
@@ -124,16 +124,81 @@ fn collect_indexes_from_tree(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Result of analyzing a pattern instance's base.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PatternBaseResult {
|
||||||
|
/// The computed base name for the pattern.
|
||||||
|
pub base: String,
|
||||||
|
/// Whether an outlier child was excluded to find the pattern.
|
||||||
|
/// If true, pattern factory should not be used.
|
||||||
|
pub has_outlier: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PatternBaseResult {
|
||||||
|
/// Returns true if an inline type should be generated instead of using a pattern factory.
|
||||||
|
///
|
||||||
|
/// This is the case when:
|
||||||
|
/// - The child fields don't match a parameterizable pattern, OR
|
||||||
|
/// - An outlier was detected during pattern analysis
|
||||||
|
pub fn should_inline(&self, is_parameterizable: bool) -> bool {
|
||||||
|
!is_parameterizable || self.has_outlier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the metric base for a pattern instance by analyzing direct children.
|
/// Get the metric base for a pattern instance by analyzing direct children.
|
||||||
///
|
///
|
||||||
/// Uses field names and first leaf names from direct children to determine
|
/// Uses field names and first leaf names from direct children to determine
|
||||||
/// the common base via `analyze_pattern_level`.
|
/// the common base via `analyze_pattern_level`.
|
||||||
pub fn get_pattern_instance_base(node: &TreeNode) -> String {
|
///
|
||||||
|
/// If the initial analysis fails to find a common pattern, it tries excluding
|
||||||
|
/// each child one at a time to detect outliers (e.g., a mismatched "base" field
|
||||||
|
/// from indexer/computed tree merging).
|
||||||
|
///
|
||||||
|
/// Returns both the base and whether an outlier was detected.
|
||||||
|
pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
|
||||||
let child_names = get_direct_children_for_analysis(node);
|
let child_names = get_direct_children_for_analysis(node);
|
||||||
if child_names.is_empty() {
|
if child_names.is_empty() {
|
||||||
return String::new();
|
return PatternBaseResult {
|
||||||
|
base: String::new(),
|
||||||
|
has_outlier: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let analysis = analyze_pattern_level(&child_names);
|
||||||
|
|
||||||
|
// If we found a common pattern, use it
|
||||||
|
if !matches!(analysis.common, CommonDenominator::None) {
|
||||||
|
return PatternBaseResult {
|
||||||
|
base: analysis.base,
|
||||||
|
has_outlier: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no common pattern found, try excluding each child one at a time
|
||||||
|
// to detect if there's a single outlier breaking the pattern.
|
||||||
|
if child_names.len() > 2 {
|
||||||
|
for i in 0..child_names.len() {
|
||||||
|
let filtered: Vec<_> = child_names
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(j, _)| *j != i)
|
||||||
|
.map(|(_, v)| v.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let filtered_analysis = analyze_pattern_level(&filtered);
|
||||||
|
if !matches!(filtered_analysis.common, CommonDenominator::None) {
|
||||||
|
return PatternBaseResult {
|
||||||
|
base: filtered_analysis.base,
|
||||||
|
has_outlier: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PatternBaseResult {
|
||||||
|
base: analysis.base,
|
||||||
|
has_outlier: false,
|
||||||
}
|
}
|
||||||
analyze_pattern_level(&child_names).base
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get (field_name, shortest_leaf_name) pairs for direct children of a branch node.
|
/// Get (field_name, shortest_leaf_name) pairs for direct children of a branch node.
|
||||||
@@ -217,3 +282,93 @@ pub fn get_fields_with_child_info(
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use brk_types::{MetricLeaf, MetricLeafWithSchema, TreeNode};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
fn make_leaf(name: &str) -> TreeNode {
|
||||||
|
let leaf = MetricLeaf {
|
||||||
|
name: name.to_string(),
|
||||||
|
kind: "TestType".to_string(),
|
||||||
|
indexes: BTreeSet::new(),
|
||||||
|
};
|
||||||
|
TreeNode::Leaf(MetricLeafWithSchema::new(leaf, serde_json::json!({})))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_branch(children: Vec<(&str, TreeNode)>) -> TreeNode {
|
||||||
|
let map: BTreeMap<String, TreeNode> = children
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), v))
|
||||||
|
.collect();
|
||||||
|
TreeNode::Branch(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_pattern_instance_base_with_base_field() {
|
||||||
|
// Simulates vbytes tree: has base field with block_vbytes leaf
|
||||||
|
let tree = make_branch(vec![
|
||||||
|
("base", make_branch(vec![("dateindex", make_leaf("block_vbytes"))])),
|
||||||
|
("average", make_branch(vec![("dateindex", make_leaf("block_vbytes_average"))])),
|
||||||
|
("sum", make_branch(vec![("dateindex", make_leaf("block_vbytes_sum"))])),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let result = get_pattern_instance_base(&tree);
|
||||||
|
assert_eq!(result.base, "block_vbytes");
|
||||||
|
assert!(!result.has_outlier);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_pattern_instance_base_without_base_field() {
|
||||||
|
// Simulates weight tree: NO base field, only suffixed metrics
|
||||||
|
let tree = make_branch(vec![
|
||||||
|
("average", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
|
||||||
|
("sum", make_branch(vec![("dateindex", make_leaf("block_weight_sum"))])),
|
||||||
|
("cumulative", make_branch(vec![("dateindex", make_leaf("block_weight_cumulative"))])),
|
||||||
|
("max", make_branch(vec![("dateindex", make_leaf("block_weight_max"))])),
|
||||||
|
("min", make_branch(vec![("dateindex", make_leaf("block_weight_min"))])),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let result = get_pattern_instance_base(&tree);
|
||||||
|
assert_eq!(result.base, "block_weight");
|
||||||
|
assert!(!result.has_outlier);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_pattern_instance_base_with_duplicate_base_field() {
|
||||||
|
// What if there's a "base" field that points to the same leaf as "average"?
|
||||||
|
// This could happen if the tree generation creates a base field that shares leaves with average
|
||||||
|
let tree = make_branch(vec![
|
||||||
|
("base", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
|
||||||
|
("average", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
|
||||||
|
("sum", make_branch(vec![("dateindex", make_leaf("block_weight_sum"))])),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let result = get_pattern_instance_base(&tree);
|
||||||
|
// Common prefix among all children is "block_weight_"
|
||||||
|
assert_eq!(result.base, "block_weight");
|
||||||
|
assert!(!result.has_outlier);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_pattern_instance_base_with_mismatched_base_name() {
|
||||||
|
// Simulates the actual bug: indexed tree's "base" field has name "weight"
|
||||||
|
// but computed tree's derived metrics use "block_weight_*" prefix.
|
||||||
|
// After tree merge, we get a base field with mismatched naming.
|
||||||
|
let tree = make_branch(vec![
|
||||||
|
("base", make_leaf("weight")), // Outlier - doesn't match pattern
|
||||||
|
("average", make_leaf("block_weight_average")),
|
||||||
|
("sum", make_leaf("block_weight_sum")),
|
||||||
|
("cumulative", make_leaf("block_weight_cumulative")),
|
||||||
|
("max", make_leaf("block_weight_max")),
|
||||||
|
("min", make_leaf("block_weight_min")),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let result = get_pattern_instance_base(&tree);
|
||||||
|
// Should detect "weight" as outlier and find common prefix from others
|
||||||
|
assert_eq!(result.base, "block_weight");
|
||||||
|
assert!(result.has_outlier); // Pattern factory should NOT be used
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,16 +4,35 @@ use std::collections::{HashMap, HashSet};
|
|||||||
|
|
||||||
use brk_types::TreeNode;
|
use brk_types::TreeNode;
|
||||||
|
|
||||||
use crate::{ClientMetadata, PatternField, get_fields_with_child_info};
|
use crate::{
|
||||||
|
ClientMetadata, PatternBaseResult, PatternField, child_type_name, get_fields_with_child_info,
|
||||||
|
get_pattern_instance_base,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Pre-computed context for a single child node.
|
||||||
|
pub struct ChildContext<'a> {
|
||||||
|
/// The child's field name in the tree.
|
||||||
|
pub name: &'a str,
|
||||||
|
/// The child node.
|
||||||
|
pub node: &'a TreeNode,
|
||||||
|
/// The field info for this child.
|
||||||
|
pub field: PatternField,
|
||||||
|
/// Child fields if this is a branch (for pattern lookup).
|
||||||
|
pub child_fields: Option<Vec<PatternField>>,
|
||||||
|
/// Pattern analysis result.
|
||||||
|
pub base_result: PatternBaseResult,
|
||||||
|
/// Whether this is a leaf node.
|
||||||
|
pub is_leaf: bool,
|
||||||
|
/// Whether to use an inline type instead of a pattern factory (only meaningful for branches).
|
||||||
|
pub should_inline: bool,
|
||||||
|
/// The type name to use for inline branches.
|
||||||
|
pub inline_type_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Context for generating a tree node, returned by `prepare_tree_node`.
|
/// Context for generating a tree node, returned by `prepare_tree_node`.
|
||||||
pub struct TreeNodeContext<'a> {
|
pub struct TreeNodeContext<'a> {
|
||||||
/// The children of the branch node.
|
/// Pre-computed context for each child.
|
||||||
pub children: &'a std::collections::BTreeMap<String, TreeNode>,
|
pub children: Vec<ChildContext<'a>>,
|
||||||
/// Fields with optional child field info for generic pattern lookup.
|
|
||||||
pub fields_with_child_info: Vec<(PatternField, Option<Vec<PatternField>>)>,
|
|
||||||
/// Just the fields (for pattern lookup).
|
|
||||||
pub fields: Vec<PatternField>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prepare a tree node for generation.
|
/// Prepare a tree node for generation.
|
||||||
@@ -26,20 +45,22 @@ pub fn prepare_tree_node<'a>(
|
|||||||
metadata: &ClientMetadata,
|
metadata: &ClientMetadata,
|
||||||
generated: &mut HashSet<String>,
|
generated: &mut HashSet<String>,
|
||||||
) -> Option<TreeNodeContext<'a>> {
|
) -> Option<TreeNodeContext<'a>> {
|
||||||
let TreeNode::Branch(children) = node else {
|
let TreeNode::Branch(branch_children) = node else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let fields_with_child_info = get_fields_with_child_info(children, name, pattern_lookup);
|
let fields_with_child_info = get_fields_with_child_info(branch_children, name, pattern_lookup);
|
||||||
let fields: Vec<PatternField> = fields_with_child_info
|
let fields: Vec<PatternField> = fields_with_child_info
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(f, _)| f.clone())
|
.map(|(f, _)| f.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Skip if this matches a parameterizable pattern
|
// Skip if this matches a parameterizable pattern AND has no outlier
|
||||||
|
let base_result = get_pattern_instance_base(node);
|
||||||
if let Some(pattern_name) = pattern_lookup.get(&fields)
|
if let Some(pattern_name) = pattern_lookup.get(&fields)
|
||||||
&& pattern_name != name
|
&& pattern_name != name
|
||||||
&& metadata.is_parameterizable(pattern_name)
|
&& metadata.is_parameterizable(pattern_name)
|
||||||
|
&& !base_result.has_outlier
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -50,9 +71,38 @@ pub fn prepare_tree_node<'a>(
|
|||||||
}
|
}
|
||||||
generated.insert(name.to_string());
|
generated.insert(name.to_string());
|
||||||
|
|
||||||
Some(TreeNodeContext {
|
// Build child contexts with pre-computed decisions
|
||||||
children,
|
let children: Vec<ChildContext<'a>> = branch_children
|
||||||
fields_with_child_info,
|
.iter()
|
||||||
fields,
|
.zip(fields_with_child_info)
|
||||||
})
|
.map(|((child_name, child_node), (field, child_fields))| {
|
||||||
|
let is_leaf = matches!(child_node, TreeNode::Leaf(_));
|
||||||
|
let base_result = get_pattern_instance_base(child_node);
|
||||||
|
let is_parameterizable = child_fields
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|cf| metadata.is_parameterizable_fields(cf));
|
||||||
|
// should_inline is only meaningful for branches
|
||||||
|
let should_inline = !is_leaf && base_result.should_inline(is_parameterizable);
|
||||||
|
|
||||||
|
// Inline type name (only used when should_inline is true)
|
||||||
|
let inline_type_name = if should_inline {
|
||||||
|
child_type_name(name, child_name)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
ChildContext {
|
||||||
|
name: child_name,
|
||||||
|
node: child_node,
|
||||||
|
field,
|
||||||
|
child_fields,
|
||||||
|
base_result,
|
||||||
|
is_leaf,
|
||||||
|
should_inline,
|
||||||
|
inline_type_name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(TreeNodeContext { children })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,57 +63,65 @@ class BrkError extends Error {{
|
|||||||
* @property {{number}} end - End index (exclusive)
|
* @property {{number}} end - End index (exclusive)
|
||||||
* @property {{T[]}} data - The metric data
|
* @property {{T[]}} data - The metric data
|
||||||
*/
|
*/
|
||||||
/** @typedef {{MetricData<unknown>}} AnyMetricData */
|
/** @typedef {{MetricData<any>}} AnyMetricData */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Thenable interface for await support.
|
||||||
|
* @template T
|
||||||
|
* @typedef {{(onfulfilled?: (value: MetricData<T>) => MetricData<T>, onrejected?: (reason: Error) => never) => Promise<MetricData<T>>}} Thenable
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metric endpoint builder. Callable (returns itself) so both .by.dateindex and .by.dateindex() work.
|
||||||
* @template T
|
* @template T
|
||||||
* @typedef {{Object}} MetricEndpointBuilder
|
* @typedef {{Object}} MetricEndpointBuilder
|
||||||
* @property {{(n: number) => RangeBuilder<T>}} first - Fetch first n data points
|
* @property {{(index: number) => SingleItemBuilder<T>}} get - Get single item at index
|
||||||
* @property {{(n: number) => RangeBuilder<T>}} last - Fetch last n data points
|
* @property {{(start?: number, end?: number) => RangeBuilder<T>}} slice - Slice like Array.slice
|
||||||
* @property {{(start: number, end: number) => RangeBuilder<T>}} range - Set explicit range [start, end)
|
* @property {{(n: number) => RangeBuilder<T>}} first - Get first n items
|
||||||
* @property {{(start: number) => FromBuilder<T>}} from - Set start position, chain with take() or to()
|
* @property {{(n: number) => RangeBuilder<T>}} last - Get last n items
|
||||||
* @property {{(end: number) => ToBuilder<T>}} to - Set end position, chain with takeLast() or from()
|
* @property {{(n: number) => SkippedBuilder<T>}} skip - Skip first n items, chain with take()
|
||||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON (all data)
|
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch all data
|
||||||
* @property {{() => Promise<string>}} csv - Execute and return CSV (all data)
|
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
|
||||||
|
* @property {{Thenable<T>}} then - Thenable (await endpoint)
|
||||||
* @property {{string}} path - The endpoint path
|
* @property {{string}} path - The endpoint path
|
||||||
*/
|
*/
|
||||||
/** @typedef {{MetricEndpointBuilder<unknown>}} AnyMetricEndpointBuilder */
|
/** @typedef {{MetricEndpointBuilder<any>}} AnyMetricEndpointBuilder */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @typedef {{Object}} FromBuilder
|
* @typedef {{Object}} SingleItemBuilder
|
||||||
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items from start position
|
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the item
|
||||||
* @property {{(end: number) => RangeBuilder<T>}} to - Set end position
|
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
|
||||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON
|
* @property {{Thenable<T>}} then - Thenable
|
||||||
* @property {{() => Promise<string>}} csv - Execute and return CSV
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @typedef {{Object}} ToBuilder
|
* @typedef {{Object}} SkippedBuilder
|
||||||
* @property {{(n: number) => RangeBuilder<T>}} takeLast - Take last n items before end position
|
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items after skipped position
|
||||||
* @property {{(start: number) => RangeBuilder<T>}} from - Set start position
|
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch from skipped position to end
|
||||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON
|
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
|
||||||
* @property {{() => Promise<string>}} csv - Execute and return CSV
|
* @property {{Thenable<T>}} then - Thenable
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @typedef {{Object}} RangeBuilder
|
* @typedef {{Object}} RangeBuilder
|
||||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON
|
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the range
|
||||||
* @property {{() => Promise<string>}} csv - Execute and return CSV
|
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
|
||||||
|
* @property {{Thenable<T>}} then - Thenable
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @typedef {{Object}} MetricPattern
|
* @typedef {{Object}} MetricPattern
|
||||||
* @property {{string}} name - The metric name
|
* @property {{string}} name - The metric name
|
||||||
* @property {{Partial<Record<Index, MetricEndpointBuilder<T>>>}} by - Index endpoints (lazy getters)
|
* @property {{Readonly<Partial<Record<Index, MetricEndpointBuilder<T>>>>}} by - Index endpoints as lazy getters. Access via .by.dateindex or .by['dateindex']
|
||||||
* @property {{() => Index[]}} indexes - Get the list of available indexes
|
* @property {{() => Index[]}} indexes - Get the list of available indexes
|
||||||
* @property {{(index: Index) => MetricEndpointBuilder<T>|undefined}} get - Get an endpoint for a specific index
|
* @property {{(index: Index) => MetricEndpointBuilder<T>|undefined}} get - Get an endpoint for a specific index
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @typedef {{MetricPattern<unknown>}} AnyMetricPattern */
|
/** @typedef {{MetricPattern<any>}} AnyMetricPattern */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a metric endpoint builder with typestate pattern.
|
* Create a metric endpoint builder with typestate pattern.
|
||||||
@@ -147,50 +155,46 @@ function _endpoint(client, name, index) {{
|
|||||||
* @returns {{RangeBuilder<T>}}
|
* @returns {{RangeBuilder<T>}}
|
||||||
*/
|
*/
|
||||||
const rangeBuilder = (start, end) => ({{
|
const rangeBuilder = (start, end) => ({{
|
||||||
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
|
fetch(onUpdate) {{ return client.getJson(buildPath(start, end), onUpdate); }},
|
||||||
return client.getJson(buildPath(start, end), onUpdate);
|
fetchCsv() {{ return client.getText(buildPath(start, end, 'csv')); }},
|
||||||
}},
|
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||||
csv() {{ return client.getText(buildPath(start, end, 'csv')); }},
|
}});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{number}} index
|
||||||
|
* @returns {{SingleItemBuilder<T>}}
|
||||||
|
*/
|
||||||
|
const singleItemBuilder = (index) => ({{
|
||||||
|
fetch(onUpdate) {{ return client.getJson(buildPath(index, index + 1), onUpdate); }},
|
||||||
|
fetchCsv() {{ return client.getText(buildPath(index, index + 1, 'csv')); }},
|
||||||
|
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||||
}});
|
}});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{number}} start
|
* @param {{number}} start
|
||||||
* @returns {{FromBuilder<T>}}
|
* @returns {{SkippedBuilder<T>}}
|
||||||
*/
|
*/
|
||||||
const fromBuilder = (start) => ({{
|
const skippedBuilder = (start) => ({{
|
||||||
take(/** @type {{number}} */ n) {{ return rangeBuilder(start, start + n); }},
|
take(n) {{ return rangeBuilder(start, start + n); }},
|
||||||
to(/** @type {{number}} */ end) {{ return rangeBuilder(start, end); }},
|
fetch(onUpdate) {{ return client.getJson(buildPath(start, undefined), onUpdate); }},
|
||||||
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
|
fetchCsv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
|
||||||
return client.getJson(buildPath(start, undefined), onUpdate);
|
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||||
}},
|
|
||||||
csv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
|
|
||||||
}});
|
}});
|
||||||
|
|
||||||
/**
|
/** @type {{MetricEndpointBuilder<T>}} */
|
||||||
* @param {{number}} end
|
const endpoint = {{
|
||||||
* @returns {{ToBuilder<T>}}
|
get(index) {{ return singleItemBuilder(index); }},
|
||||||
*/
|
slice(start, end) {{ return rangeBuilder(start, end); }},
|
||||||
const toBuilder = (end) => ({{
|
first(n) {{ return rangeBuilder(undefined, n); }},
|
||||||
takeLast(/** @type {{number}} */ n) {{ return rangeBuilder(end - n, end); }},
|
last(n) {{ return rangeBuilder(-n, undefined); }},
|
||||||
from(/** @type {{number}} */ start) {{ return rangeBuilder(start, end); }},
|
skip(n) {{ return skippedBuilder(n); }},
|
||||||
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
|
fetch(onUpdate) {{ return client.getJson(buildPath(), onUpdate); }},
|
||||||
return client.getJson(buildPath(undefined, end), onUpdate);
|
fetchCsv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
|
||||||
}},
|
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||||
csv() {{ return client.getText(buildPath(undefined, end, 'csv')); }},
|
|
||||||
}});
|
|
||||||
|
|
||||||
return {{
|
|
||||||
first(/** @type {{number}} */ n) {{ return rangeBuilder(undefined, n); }},
|
|
||||||
last(/** @type {{number}} */ n) {{ return rangeBuilder(-n, undefined); }},
|
|
||||||
range(/** @type {{number}} */ start, /** @type {{number}} */ end) {{ return rangeBuilder(start, end); }},
|
|
||||||
from(/** @type {{number}} */ start) {{ return fromBuilder(start); }},
|
|
||||||
to(/** @type {{number}} */ end) {{ return toBuilder(end); }},
|
|
||||||
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
|
|
||||||
return client.getJson(buildPath(), onUpdate);
|
|
||||||
}},
|
|
||||||
csv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
|
|
||||||
get path() {{ return p; }},
|
get path() {{ return p; }},
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
return endpoint;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -235,7 +239,7 @@ class BrkClientBase {{
|
|||||||
const cachedJson = cachedRes ? await cachedRes.json() : null;
|
const cachedJson = cachedRes ? await cachedRes.json() : null;
|
||||||
|
|
||||||
if (cachedJson) onUpdate?.(cachedJson);
|
if (cachedJson) onUpdate?.(cachedJson);
|
||||||
if (!globalThis.navigator?.onLine) {{
|
if (globalThis.navigator?.onLine === false) {{
|
||||||
if (cachedJson) return cachedJson;
|
if (cachedJson) return cachedJson;
|
||||||
throw new BrkError('Offline and no cached data available');
|
throw new BrkError('Offline and no cached data available');
|
||||||
}}
|
}}
|
||||||
@@ -305,7 +309,11 @@ pub fn generate_static_constants(output: &mut String) {
|
|||||||
fn instance_const_camel<T: Serialize>(output: &mut String, name: &str, value: &T) {
|
fn instance_const_camel<T: Serialize>(output: &mut String, name: &str, value: &T) {
|
||||||
let json_value: Value = serde_json::to_value(value).unwrap();
|
let json_value: Value = serde_json::to_value(value).unwrap();
|
||||||
let camel_value = camel_case_top_level_keys(json_value);
|
let camel_value = camel_case_top_level_keys(json_value);
|
||||||
write_static_const(output, name, &serde_json::to_string_pretty(&camel_value).unwrap());
|
write_static_const(
|
||||||
|
output,
|
||||||
|
name,
|
||||||
|
&serde_json::to_string_pretty(&camel_value).unwrap(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
instance_const_camel(output, "TERM_NAMES", &TERM_NAMES);
|
instance_const_camel(output, "TERM_NAMES", &TERM_NAMES);
|
||||||
@@ -336,13 +344,25 @@ fn camel_case_top_level_keys(value: Value) -> Value {
|
|||||||
fn indent_json_const(json: &str) -> String {
|
fn indent_json_const(json: &str) -> String {
|
||||||
json.lines()
|
json.lines()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, line)| if i == 0 { line.to_string() } else { format!(" {}", line) })
|
.map(|(i, line)| {
|
||||||
|
if i == 0 {
|
||||||
|
line.to_string()
|
||||||
|
} else {
|
||||||
|
format!(" {}", line)
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_static_const(output: &mut String, name: &str, json: &str) {
|
fn write_static_const(output: &mut String, name: &str, json: &str) {
|
||||||
writeln!(output, " {} = /** @type {{const}} */ ({});\n", name, indent_json_const(json)).unwrap();
|
writeln!(
|
||||||
|
output,
|
||||||
|
" {} = /** @type {{const}} */ ({});\n",
|
||||||
|
name,
|
||||||
|
indent_json_const(json)
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate index accessor factory functions.
|
/// Generate index accessor factory functions.
|
||||||
@@ -354,14 +374,30 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
|
|||||||
writeln!(output, "// Index accessor factory functions\n").unwrap();
|
writeln!(output, "// Index accessor factory functions\n").unwrap();
|
||||||
|
|
||||||
for pattern in patterns {
|
for pattern in patterns {
|
||||||
|
// Use 'readonly' to indicate these are getters (lazy evaluation)
|
||||||
let by_fields: Vec<String> = pattern
|
let by_fields: Vec<String> = pattern
|
||||||
.indexes
|
.indexes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|idx| format!("{}: MetricEndpointBuilder<T>", idx.serialize_long()))
|
.map(|idx| {
|
||||||
|
format!(
|
||||||
|
"readonly {}: MetricEndpointBuilder<T>",
|
||||||
|
idx.serialize_long()
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let by_type = format!("{{ {} }}", by_fields.join(", "));
|
let by_type = format!("{{ {} }}", by_fields.join(", "));
|
||||||
|
|
||||||
writeln!(output, "/**").unwrap();
|
writeln!(output, "/**").unwrap();
|
||||||
|
writeln!(
|
||||||
|
output,
|
||||||
|
" * Metric pattern with index endpoints as lazy getters."
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
writeln!(
|
||||||
|
output,
|
||||||
|
" * Access via property (.by.dateindex) or bracket notation (.by['dateindex'])."
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
writeln!(output, " * @template T").unwrap();
|
writeln!(output, " * @template T").unwrap();
|
||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
@@ -385,7 +421,11 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
|
|||||||
|
|
||||||
for (i, index) in pattern.indexes.iter().enumerate() {
|
for (i, index) in pattern.indexes.iter().enumerate() {
|
||||||
let index_name = index.serialize_long();
|
let index_name = index.serialize_long();
|
||||||
let comma = if i < pattern.indexes.len() - 1 { "," } else { "" };
|
let comma = if i < pattern.indexes.len() - 1 {
|
||||||
|
","
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
" get {}() {{ return _endpoint(client, name, '{}'); }}{}",
|
" get {}() {{ return _endpoint(client, name, '{}'); }}{}",
|
||||||
@@ -438,8 +478,12 @@ pub fn generate_structural_patterns(
|
|||||||
}
|
}
|
||||||
writeln!(output, " * @typedef {{Object}} {}", pattern.name).unwrap();
|
writeln!(output, " * @typedef {{Object}} {}", pattern.name).unwrap();
|
||||||
for field in &pattern.fields {
|
for field in &pattern.fields {
|
||||||
let js_type =
|
let js_type = metadata.field_type_annotation(
|
||||||
metadata.field_type_annotation(field, pattern.is_generic, None, GenericSyntax::JAVASCRIPT);
|
field,
|
||||||
|
pattern.is_generic,
|
||||||
|
None,
|
||||||
|
GenericSyntax::JAVASCRIPT,
|
||||||
|
);
|
||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
" * @property {{{}}} {}",
|
" * @property {{{}}} {}",
|
||||||
@@ -469,8 +513,17 @@ pub fn generate_structural_patterns(
|
|||||||
writeln!(output, " * @returns {{{}}}", return_type).unwrap();
|
writeln!(output, " * @returns {{{}}}", return_type).unwrap();
|
||||||
writeln!(output, " */").unwrap();
|
writeln!(output, " */").unwrap();
|
||||||
|
|
||||||
let param_name = if is_parameterizable { "acc" } else { "basePath" };
|
let param_name = if is_parameterizable {
|
||||||
writeln!(output, "function create{}(client, {}) {{", pattern.name, param_name).unwrap();
|
"acc"
|
||||||
|
} else {
|
||||||
|
"basePath"
|
||||||
|
};
|
||||||
|
writeln!(
|
||||||
|
output,
|
||||||
|
"function create{}(client, {}) {{",
|
||||||
|
pattern.name, param_name
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
writeln!(output, " return {{").unwrap();
|
writeln!(output, " return {{").unwrap();
|
||||||
|
|
||||||
let syntax = JavaScriptSyntax;
|
let syntax = JavaScriptSyntax;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::fmt::Write;
|
|||||||
use brk_types::TreeNode;
|
use brk_types::TreeNode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ClientMetadata, Endpoint, GenericSyntax, JavaScriptSyntax, PatternField, child_type_name,
|
ClientMetadata, Endpoint, GenericSyntax, JavaScriptSyntax, PatternField,
|
||||||
generate_leaf_field, get_first_leaf_name, get_node_fields, get_pattern_instance_base,
|
generate_leaf_field, get_first_leaf_name, get_node_fields, get_pattern_instance_base,
|
||||||
infer_accumulated_name, prepare_tree_node, to_camel_case,
|
infer_accumulated_name, prepare_tree_node, to_camel_case,
|
||||||
};
|
};
|
||||||
@@ -45,43 +45,41 @@ fn generate_tree_typedef(
|
|||||||
writeln!(output, "/**").unwrap();
|
writeln!(output, "/**").unwrap();
|
||||||
writeln!(output, " * @typedef {{Object}} {}", name).unwrap();
|
writeln!(output, " * @typedef {{Object}} {}", name).unwrap();
|
||||||
|
|
||||||
for ((field, child_fields), (child_name, _)) in
|
for child in &ctx.children {
|
||||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
let js_type = if child.should_inline {
|
||||||
{
|
child.inline_type_name.clone()
|
||||||
let js_type = metadata.resolve_tree_field_type(
|
} else {
|
||||||
field,
|
metadata.resolve_tree_field_type(
|
||||||
child_fields.as_deref(),
|
&child.field,
|
||||||
name,
|
child.child_fields.as_deref(),
|
||||||
child_name,
|
name,
|
||||||
GenericSyntax::JAVASCRIPT,
|
child.name,
|
||||||
);
|
GenericSyntax::JAVASCRIPT,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
" * @property {{{}}} {}",
|
" * @property {{{}}} {}",
|
||||||
js_type,
|
js_type,
|
||||||
to_camel_case(&field.name)
|
to_camel_case(&child.field.name)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
writeln!(output, " */\n").unwrap();
|
writeln!(output, " */\n").unwrap();
|
||||||
|
|
||||||
for (child_name, child_node) in ctx.children {
|
// Generate child typedefs
|
||||||
if let TreeNode::Branch(grandchildren) = child_node {
|
for child in &ctx.children {
|
||||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
if child.should_inline {
|
||||||
// Generate typedef if no pattern match OR pattern is not parameterizable
|
generate_tree_typedef(
|
||||||
if !metadata.is_parameterizable_fields(&child_fields) {
|
output,
|
||||||
let child_type = child_type_name(name, child_name);
|
&child.inline_type_name,
|
||||||
generate_tree_typedef(
|
child.node,
|
||||||
output,
|
pattern_lookup,
|
||||||
&child_type,
|
metadata,
|
||||||
child_node,
|
generated,
|
||||||
pattern_lookup,
|
);
|
||||||
metadata,
|
|
||||||
generated,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,12 +180,14 @@ fn generate_tree_initializer(
|
|||||||
.get(&child_fields)
|
.get(&child_fields)
|
||||||
.filter(|name| metadata.is_parameterizable(name));
|
.filter(|name| metadata.is_parameterizable(name));
|
||||||
|
|
||||||
if let Some(pattern_name) = pattern_name {
|
let base_result = get_pattern_instance_base(child_node);
|
||||||
let arg = get_pattern_instance_base(child_node);
|
|
||||||
|
// Use pattern factory only if no outlier was detected
|
||||||
|
if let Some(pattern_name) = pattern_name.filter(|_| !base_result.has_outlier) {
|
||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
"{}{}: create{}(this, '{}'),",
|
"{}{}: create{}(this, '{}'),",
|
||||||
indent_str, field_name, pattern_name, arg
|
indent_str, field_name, pattern_name, base_result.base
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -195,143 +195,138 @@ class _EndpointConfig:
|
|||||||
|
|
||||||
|
|
||||||
class RangeBuilder(Generic[T]):
|
class RangeBuilder(Generic[T]):
|
||||||
"""Final builder with range fully specified. Can only call json() or csv()."""
|
"""Builder with range specified."""
|
||||||
|
|
||||||
def __init__(self, config: _EndpointConfig):
|
def __init__(self, config: _EndpointConfig):
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
def json(self) -> MetricData[T]:
|
def fetch(self) -> MetricData[T]:
|
||||||
"""Execute the query and return parsed JSON data."""
|
"""Fetch the range as parsed JSON."""
|
||||||
return self._config.get_json()
|
return self._config.get_json()
|
||||||
|
|
||||||
def csv(self) -> str:
|
def fetch_csv(self) -> str:
|
||||||
"""Execute the query and return CSV data as a string."""
|
"""Fetch the range as CSV string."""
|
||||||
return self._config.get_csv()
|
return self._config.get_csv()
|
||||||
|
|
||||||
|
|
||||||
class FromBuilder(Generic[T]):
|
class SingleItemBuilder(Generic[T]):
|
||||||
"""Builder after calling from(start). Can chain with take() or to()."""
|
"""Builder for single item access."""
|
||||||
|
|
||||||
|
def __init__(self, config: _EndpointConfig):
|
||||||
|
self._config = config
|
||||||
|
|
||||||
|
def fetch(self) -> MetricData[T]:
|
||||||
|
"""Fetch the single item."""
|
||||||
|
return self._config.get_json()
|
||||||
|
|
||||||
|
def fetch_csv(self) -> str:
|
||||||
|
"""Fetch as CSV."""
|
||||||
|
return self._config.get_csv()
|
||||||
|
|
||||||
|
|
||||||
|
class SkippedBuilder(Generic[T]):
|
||||||
|
"""Builder after calling skip(n). Chain with take() to specify count."""
|
||||||
|
|
||||||
def __init__(self, config: _EndpointConfig):
|
def __init__(self, config: _EndpointConfig):
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
def take(self, n: int) -> RangeBuilder[T]:
|
def take(self, n: int) -> RangeBuilder[T]:
|
||||||
"""Take n items from the start position."""
|
"""Take n items after the skipped position."""
|
||||||
start = self._config.start or 0
|
start = self._config.start or 0
|
||||||
return RangeBuilder(_EndpointConfig(
|
return RangeBuilder(_EndpointConfig(
|
||||||
self._config.client, self._config.name, self._config.index,
|
self._config.client, self._config.name, self._config.index,
|
||||||
start, start + n
|
start, start + n
|
||||||
))
|
))
|
||||||
|
|
||||||
def to(self, end: int) -> RangeBuilder[T]:
|
def fetch(self) -> MetricData[T]:
|
||||||
"""Set the end position."""
|
"""Fetch from skipped position to end."""
|
||||||
return RangeBuilder(_EndpointConfig(
|
|
||||||
self._config.client, self._config.name, self._config.index,
|
|
||||||
self._config.start, end
|
|
||||||
))
|
|
||||||
|
|
||||||
def json(self) -> MetricData[T]:
|
|
||||||
"""Execute the query and return parsed JSON data (from start to end of data)."""
|
|
||||||
return self._config.get_json()
|
return self._config.get_json()
|
||||||
|
|
||||||
def csv(self) -> str:
|
def fetch_csv(self) -> str:
|
||||||
"""Execute the query and return CSV data as a string."""
|
"""Fetch as CSV."""
|
||||||
return self._config.get_csv()
|
|
||||||
|
|
||||||
|
|
||||||
class ToBuilder(Generic[T]):
|
|
||||||
"""Builder after calling to(end). Can chain with take_last() or from()."""
|
|
||||||
|
|
||||||
def __init__(self, config: _EndpointConfig):
|
|
||||||
self._config = config
|
|
||||||
|
|
||||||
def take_last(self, n: int) -> RangeBuilder[T]:
|
|
||||||
"""Take last n items before the end position."""
|
|
||||||
end = self._config.end or 0
|
|
||||||
return RangeBuilder(_EndpointConfig(
|
|
||||||
self._config.client, self._config.name, self._config.index,
|
|
||||||
end - n, end
|
|
||||||
))
|
|
||||||
|
|
||||||
def from_(self, start: int) -> RangeBuilder[T]:
|
|
||||||
"""Set the start position."""
|
|
||||||
return RangeBuilder(_EndpointConfig(
|
|
||||||
self._config.client, self._config.name, self._config.index,
|
|
||||||
start, self._config.end
|
|
||||||
))
|
|
||||||
|
|
||||||
def json(self) -> MetricData[T]:
|
|
||||||
"""Execute the query and return parsed JSON data (from start of data to end)."""
|
|
||||||
return self._config.get_json()
|
|
||||||
|
|
||||||
def csv(self) -> str:
|
|
||||||
"""Execute the query and return CSV data as a string."""
|
|
||||||
return self._config.get_csv()
|
return self._config.get_csv()
|
||||||
|
|
||||||
|
|
||||||
class MetricEndpointBuilder(Generic[T]):
|
class MetricEndpointBuilder(Generic[T]):
|
||||||
"""Initial builder for metric endpoint queries.
|
"""Builder for metric endpoint queries.
|
||||||
|
|
||||||
Use method chaining to specify the data range, then call json() or csv() to execute.
|
Use method chaining to specify the data range, then call fetch() or fetch_csv() to execute.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
# Get all data
|
# Fetch all data
|
||||||
endpoint.json()
|
data = endpoint.fetch()
|
||||||
|
|
||||||
# Get last 10 points
|
# Single item access
|
||||||
endpoint.last(10).json()
|
data = endpoint[5].fetch()
|
||||||
|
|
||||||
# Get range [100, 200)
|
# Slice syntax (Python-native)
|
||||||
endpoint.range(100, 200).json()
|
data = endpoint[:10].fetch() # First 10
|
||||||
|
data = endpoint[-5:].fetch() # Last 5
|
||||||
|
data = endpoint[100:110].fetch() # Range
|
||||||
|
|
||||||
# Get 10 points starting from position 100
|
# Convenience methods (pandas-style)
|
||||||
endpoint.from_(100).take(10).json()
|
data = endpoint.head().fetch() # First 10 (default)
|
||||||
|
data = endpoint.head(20).fetch() # First 20
|
||||||
|
data = endpoint.tail(5).fetch() # Last 5
|
||||||
|
|
||||||
|
# Iterator-style chaining
|
||||||
|
data = endpoint.skip(100).take(10).fetch()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, client: BrkClientBase, name: str, index: Index):
|
def __init__(self, client: BrkClientBase, name: str, index: Index):
|
||||||
self._config = _EndpointConfig(client, name, index)
|
self._config = _EndpointConfig(client, name, index)
|
||||||
|
|
||||||
def first(self, n: int) -> RangeBuilder[T]:
|
@overload
|
||||||
"""Fetch the first n data points."""
|
def __getitem__(self, key: int) -> SingleItemBuilder[T]: ...
|
||||||
|
@overload
|
||||||
|
def __getitem__(self, key: slice) -> RangeBuilder[T]: ...
|
||||||
|
|
||||||
|
def __getitem__(self, key: Union[int, slice]) -> Union[SingleItemBuilder[T], RangeBuilder[T]]:
|
||||||
|
"""Access single item or slice.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
endpoint[5] # Single item at index 5
|
||||||
|
endpoint[:10] # First 10
|
||||||
|
endpoint[-5:] # Last 5
|
||||||
|
endpoint[100:110] # Range 100-109
|
||||||
|
"""
|
||||||
|
if isinstance(key, int):
|
||||||
|
return SingleItemBuilder(_EndpointConfig(
|
||||||
|
self._config.client, self._config.name, self._config.index,
|
||||||
|
key, key + 1
|
||||||
|
))
|
||||||
|
return RangeBuilder(_EndpointConfig(
|
||||||
|
self._config.client, self._config.name, self._config.index,
|
||||||
|
key.start, key.stop
|
||||||
|
))
|
||||||
|
|
||||||
|
def head(self, n: int = 10) -> RangeBuilder[T]:
|
||||||
|
"""Get the first n items (pandas-style)."""
|
||||||
return RangeBuilder(_EndpointConfig(
|
return RangeBuilder(_EndpointConfig(
|
||||||
self._config.client, self._config.name, self._config.index,
|
self._config.client, self._config.name, self._config.index,
|
||||||
None, n
|
None, n
|
||||||
))
|
))
|
||||||
|
|
||||||
def last(self, n: int) -> RangeBuilder[T]:
|
def tail(self, n: int = 10) -> RangeBuilder[T]:
|
||||||
"""Fetch the last n data points."""
|
"""Get the last n items (pandas-style)."""
|
||||||
return RangeBuilder(_EndpointConfig(
|
return RangeBuilder(_EndpointConfig(
|
||||||
self._config.client, self._config.name, self._config.index,
|
self._config.client, self._config.name, self._config.index,
|
||||||
-n, None
|
-n, None
|
||||||
))
|
))
|
||||||
|
|
||||||
def range(self, start: int, end: int) -> RangeBuilder[T]:
|
def skip(self, n: int) -> SkippedBuilder[T]:
|
||||||
"""Set an explicit range [start, end)."""
|
"""Skip the first n items. Chain with take() to get a range."""
|
||||||
return RangeBuilder(_EndpointConfig(
|
return SkippedBuilder(_EndpointConfig(
|
||||||
self._config.client, self._config.name, self._config.index,
|
self._config.client, self._config.name, self._config.index,
|
||||||
start, end
|
n, None
|
||||||
))
|
))
|
||||||
|
|
||||||
def from_(self, start: int) -> FromBuilder[T]:
|
def fetch(self) -> MetricData[T]:
|
||||||
"""Set the start position. Chain with take() or to()."""
|
"""Fetch all data as parsed JSON."""
|
||||||
return FromBuilder(_EndpointConfig(
|
|
||||||
self._config.client, self._config.name, self._config.index,
|
|
||||||
start, None
|
|
||||||
))
|
|
||||||
|
|
||||||
def to(self, end: int) -> ToBuilder[T]:
|
|
||||||
"""Set the end position. Chain with take_last() or from_()."""
|
|
||||||
return ToBuilder(_EndpointConfig(
|
|
||||||
self._config.client, self._config.name, self._config.index,
|
|
||||||
None, end
|
|
||||||
))
|
|
||||||
|
|
||||||
def json(self) -> MetricData[T]:
|
|
||||||
"""Execute the query and return parsed JSON data (all data)."""
|
|
||||||
return self._config.get_json()
|
return self._config.get_json()
|
||||||
|
|
||||||
def csv(self) -> str:
|
def fetch_csv(self) -> str:
|
||||||
"""Execute the query and return CSV data as a string (all data)."""
|
"""Fetch all data as CSV string."""
|
||||||
return self._config.get_csv()
|
return self._config.get_csv()
|
||||||
|
|
||||||
def path(self) -> str:
|
def path(self) -> str:
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ pub fn generate_python_client(
|
|||||||
writeln!(output, "# Do not edit manually\n").unwrap();
|
writeln!(output, "# Do not edit manually\n").unwrap();
|
||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol"
|
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol, overload"
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
writeln!(output, "from http.client import HTTPSConnection, HTTPConnection").unwrap();
|
writeln!(output, "from http.client import HTTPSConnection, HTTPConnection").unwrap();
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use std::fmt::Write;
|
|||||||
use brk_types::TreeNode;
|
use brk_types::TreeNode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, child_type_name, generate_leaf_field,
|
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, generate_leaf_field,
|
||||||
get_node_fields, get_pattern_instance_base, prepare_tree_node, to_snake_case,
|
prepare_tree_node, to_snake_case,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Generate tree classes
|
/// Generate tree classes
|
||||||
@@ -41,22 +41,16 @@ fn generate_tree_class(
|
|||||||
|
|
||||||
// Generate child classes FIRST (post-order traversal)
|
// Generate child classes FIRST (post-order traversal)
|
||||||
// This ensures children are defined before parent references them
|
// This ensures children are defined before parent references them
|
||||||
for (child_name, child_node) in ctx.children.iter() {
|
for child in &ctx.children {
|
||||||
if let TreeNode::Branch(grandchildren) = child_node {
|
if child.should_inline {
|
||||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
generate_tree_class(
|
||||||
|
output,
|
||||||
// Generate inline class if no pattern match OR pattern is not parameterizable
|
&child.inline_type_name,
|
||||||
if !metadata.is_parameterizable_fields(&child_fields) {
|
child.node,
|
||||||
let child_class = child_type_name(name, child_name);
|
pattern_lookup,
|
||||||
generate_tree_class(
|
metadata,
|
||||||
output,
|
generated,
|
||||||
&child_class,
|
);
|
||||||
child_node,
|
|
||||||
pattern_lookup,
|
|
||||||
metadata,
|
|
||||||
generated,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,45 +65,36 @@ fn generate_tree_class(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let syntax = PythonSyntax;
|
let syntax = PythonSyntax;
|
||||||
for ((field, child_fields_opt), (child_name, child_node)) in
|
for child in &ctx.children {
|
||||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
let field_name_py = to_snake_case(child.name);
|
||||||
{
|
|
||||||
let py_type = metadata.resolve_tree_field_type(
|
|
||||||
field,
|
|
||||||
child_fields_opt.as_deref(),
|
|
||||||
name,
|
|
||||||
child_name,
|
|
||||||
GenericSyntax::PYTHON,
|
|
||||||
);
|
|
||||||
let field_name_py = to_snake_case(&field.name);
|
|
||||||
|
|
||||||
if metadata.is_pattern_type(&field.rust_type) && metadata.is_parameterizable(&field.rust_type)
|
if child.is_leaf {
|
||||||
{
|
if let TreeNode::Leaf(leaf) = child.node {
|
||||||
// Parameterizable pattern: use pattern class with metric base
|
generate_leaf_field(output, &syntax, "client", child.name, leaf, metadata, " ");
|
||||||
let metric_base = get_pattern_instance_base(child_node);
|
}
|
||||||
writeln!(
|
} else if child.should_inline {
|
||||||
output,
|
// Inline class
|
||||||
" self.{}: {} = {}(client, '{}')",
|
|
||||||
field_name_py, py_type, field.rust_type, metric_base
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
} else if let TreeNode::Leaf(leaf) = child_node {
|
|
||||||
// Leaf node: use shared helper
|
|
||||||
generate_leaf_field(output, &syntax, "client", child_name, leaf, metadata, " ");
|
|
||||||
} else if field.is_branch() {
|
|
||||||
// Non-parameterizable pattern or regular branch: generate inline class
|
|
||||||
let inline_class = child_type_name(name, &field.name);
|
|
||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
" self.{}: {} = {}(client)",
|
" self.{}: {} = {}(client)",
|
||||||
field_name_py, inline_class, inline_class
|
field_name_py, child.inline_type_name, child.inline_type_name
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
} else {
|
} else {
|
||||||
panic!(
|
// Use pattern class with metric base
|
||||||
"Field '{}' has no matching index pattern. All metrics must be indexed.",
|
let py_type = metadata.resolve_tree_field_type(
|
||||||
field.name
|
&child.field,
|
||||||
|
child.child_fields.as_deref(),
|
||||||
|
name,
|
||||||
|
child.name,
|
||||||
|
GenericSyntax::PYTHON,
|
||||||
);
|
);
|
||||||
|
writeln!(
|
||||||
|
output,
|
||||||
|
" self.{}: {} = {}(client, '{}')",
|
||||||
|
field_name_py, py_type, child.field.rust_type, child.base_result.base
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub fn generate_imports(output: &mut String) {
|
|||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
r#"use std::sync::Arc;
|
r#"use std::sync::Arc;
|
||||||
|
use std::ops::{{Bound, RangeBounds}};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
pub use brk_cohort::*;
|
pub use brk_cohort::*;
|
||||||
pub use brk_types::*;
|
pub use brk_types::*;
|
||||||
@@ -193,21 +194,30 @@ impl EndpointConfig {{
|
|||||||
|
|
||||||
/// Initial builder for metric endpoint queries.
|
/// Initial builder for metric endpoint queries.
|
||||||
///
|
///
|
||||||
/// Use method chaining to specify the data range, then call `json()` or `csv()` to execute.
|
/// Use method chaining to specify the data range, then call `fetch()` or `fetch_csv()` to execute.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// // Get all data
|
/// // Fetch all data
|
||||||
/// endpoint.json()?;
|
/// let data = endpoint.fetch()?;
|
||||||
///
|
///
|
||||||
/// // Get last 10 points
|
/// // Get single item at index 5
|
||||||
/// endpoint.last(10).json()?;
|
/// let data = endpoint.get(5).fetch()?;
|
||||||
|
///
|
||||||
|
/// // Get first 10 using range
|
||||||
|
/// let data = endpoint.range(..10).fetch()?;
|
||||||
///
|
///
|
||||||
/// // Get range [100, 200)
|
/// // Get range [100, 200)
|
||||||
/// endpoint.range(100, 200).json()?;
|
/// let data = endpoint.range(100..200).fetch()?;
|
||||||
///
|
///
|
||||||
/// // Get 10 points starting from position 100
|
/// // Get first 10 (convenience)
|
||||||
/// endpoint.from(100).take(10).json()?;
|
/// let data = endpoint.take(10).fetch()?;
|
||||||
|
///
|
||||||
|
/// // Get last 10
|
||||||
|
/// let data = endpoint.last(10).fetch()?;
|
||||||
|
///
|
||||||
|
/// // Iterator-style chaining
|
||||||
|
/// let data = endpoint.skip(100).take(10).fetch()?;
|
||||||
/// ```
|
/// ```
|
||||||
pub struct MetricEndpointBuilder<T> {{
|
pub struct MetricEndpointBuilder<T> {{
|
||||||
config: EndpointConfig,
|
config: EndpointConfig,
|
||||||
@@ -219,44 +229,59 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
|
|||||||
Self {{ config: EndpointConfig::new(client, name, index), _marker: std::marker::PhantomData }}
|
Self {{ config: EndpointConfig::new(client, name, index), _marker: std::marker::PhantomData }}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/// Fetch the first n data points.
|
/// Select a specific index position.
|
||||||
pub fn first(mut self, n: u64) -> RangeBuilder<T> {{
|
pub fn get(mut self, index: usize) -> SingleItemBuilder<T> {{
|
||||||
self.config.end = Some(n as i64);
|
self.config.start = Some(index as i64);
|
||||||
|
self.config.end = Some(index as i64 + 1);
|
||||||
|
SingleItemBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||||
|
}}
|
||||||
|
|
||||||
|
/// Select a range using Rust range syntax.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```ignore
|
||||||
|
/// endpoint.range(..10) // first 10
|
||||||
|
/// endpoint.range(100..110) // indices 100-109
|
||||||
|
/// endpoint.range(100..) // from 100 to end
|
||||||
|
/// ```
|
||||||
|
pub fn range<R: RangeBounds<usize>>(mut self, range: R) -> RangeBuilder<T> {{
|
||||||
|
self.config.start = match range.start_bound() {{
|
||||||
|
Bound::Included(&n) => Some(n as i64),
|
||||||
|
Bound::Excluded(&n) => Some(n as i64 + 1),
|
||||||
|
Bound::Unbounded => None,
|
||||||
|
}};
|
||||||
|
self.config.end = match range.end_bound() {{
|
||||||
|
Bound::Included(&n) => Some(n as i64 + 1),
|
||||||
|
Bound::Excluded(&n) => Some(n as i64),
|
||||||
|
Bound::Unbounded => None,
|
||||||
|
}};
|
||||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/// Fetch the last n data points.
|
/// Take the first n items.
|
||||||
pub fn last(mut self, n: u64) -> RangeBuilder<T> {{
|
pub fn take(self, n: usize) -> RangeBuilder<T> {{
|
||||||
|
self.range(..n)
|
||||||
|
}}
|
||||||
|
|
||||||
|
/// Take the last n items.
|
||||||
|
pub fn last(mut self, n: usize) -> RangeBuilder<T> {{
|
||||||
self.config.start = Some(-(n as i64));
|
self.config.start = Some(-(n as i64));
|
||||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/// Set an explicit range [start, end).
|
/// Skip the first n items. Chain with `take(n)` to get a range.
|
||||||
pub fn range(mut self, start: i64, end: i64) -> RangeBuilder<T> {{
|
pub fn skip(mut self, n: usize) -> SkippedBuilder<T> {{
|
||||||
self.config.start = Some(start);
|
self.config.start = Some(n as i64);
|
||||||
self.config.end = Some(end);
|
SkippedBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/// Set the start position. Chain with `take(n)` or `to(end)`.
|
/// Fetch all data as parsed JSON.
|
||||||
pub fn from(mut self, start: i64) -> FromBuilder<T> {{
|
pub fn fetch(self) -> Result<MetricData<T>> {{
|
||||||
self.config.start = Some(start);
|
|
||||||
FromBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
|
||||||
}}
|
|
||||||
|
|
||||||
/// Set the end position. Chain with `takeLast(n)` or `from(start)`.
|
|
||||||
pub fn to(mut self, end: i64) -> ToBuilder<T> {{
|
|
||||||
self.config.end = Some(end);
|
|
||||||
ToBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
|
||||||
}}
|
|
||||||
|
|
||||||
/// Execute the query and return parsed JSON data (all data).
|
|
||||||
pub fn json(self) -> Result<MetricData<T>> {{
|
|
||||||
self.config.get_json(None)
|
self.config.get_json(None)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/// Execute the query and return CSV data as a string (all data).
|
/// Fetch all data as CSV string.
|
||||||
pub fn csv(self) -> Result<String> {{
|
pub fn fetch_csv(self) -> Result<String> {{
|
||||||
self.config.get_text(Some("csv"))
|
self.config.get_text(Some("csv"))
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@@ -266,82 +291,63 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
|
|||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/// Builder after calling `from(start)`. Can chain with `take(n)` or `to(end)`.
|
/// Builder for single item access.
|
||||||
pub struct FromBuilder<T> {{
|
pub struct SingleItemBuilder<T> {{
|
||||||
config: EndpointConfig,
|
config: EndpointConfig,
|
||||||
_marker: std::marker::PhantomData<T>,
|
_marker: std::marker::PhantomData<T>,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
impl<T: DeserializeOwned> FromBuilder<T> {{
|
impl<T: DeserializeOwned> SingleItemBuilder<T> {{
|
||||||
/// Take n items from the start position.
|
/// Fetch the single item.
|
||||||
pub fn take(mut self, n: u64) -> RangeBuilder<T> {{
|
pub fn fetch(self) -> Result<MetricData<T>> {{
|
||||||
|
self.config.get_json(None)
|
||||||
|
}}
|
||||||
|
|
||||||
|
/// Fetch the single item as CSV.
|
||||||
|
pub fn fetch_csv(self) -> Result<String> {{
|
||||||
|
self.config.get_text(Some("csv"))
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
/// Builder after calling `skip(n)`. Chain with `take(n)` to specify count.
|
||||||
|
pub struct SkippedBuilder<T> {{
|
||||||
|
config: EndpointConfig,
|
||||||
|
_marker: std::marker::PhantomData<T>,
|
||||||
|
}}
|
||||||
|
|
||||||
|
impl<T: DeserializeOwned> SkippedBuilder<T> {{
|
||||||
|
/// Take n items after the skipped position.
|
||||||
|
pub fn take(mut self, n: usize) -> RangeBuilder<T> {{
|
||||||
let start = self.config.start.unwrap_or(0);
|
let start = self.config.start.unwrap_or(0);
|
||||||
self.config.end = Some(start + n as i64);
|
self.config.end = Some(start + n as i64);
|
||||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/// Set the end position.
|
/// Fetch from the skipped position to the end.
|
||||||
pub fn to(mut self, end: i64) -> RangeBuilder<T> {{
|
pub fn fetch(self) -> Result<MetricData<T>> {{
|
||||||
self.config.end = Some(end);
|
|
||||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
|
||||||
}}
|
|
||||||
|
|
||||||
/// Execute the query and return parsed JSON data (from start to end of data).
|
|
||||||
pub fn json(self) -> Result<MetricData<T>> {{
|
|
||||||
self.config.get_json(None)
|
self.config.get_json(None)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/// Execute the query and return CSV data as a string.
|
/// Fetch from the skipped position to the end as CSV.
|
||||||
pub fn csv(self) -> Result<String> {{
|
pub fn fetch_csv(self) -> Result<String> {{
|
||||||
self.config.get_text(Some("csv"))
|
self.config.get_text(Some("csv"))
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/// Builder after calling `to(end)`. Can chain with `takeLast(n)` or `from(start)`.
|
/// Builder with range fully specified.
|
||||||
pub struct ToBuilder<T> {{
|
|
||||||
config: EndpointConfig,
|
|
||||||
_marker: std::marker::PhantomData<T>,
|
|
||||||
}}
|
|
||||||
|
|
||||||
impl<T: DeserializeOwned> ToBuilder<T> {{
|
|
||||||
/// Take last n items before the end position.
|
|
||||||
pub fn take_last(mut self, n: u64) -> RangeBuilder<T> {{
|
|
||||||
let end = self.config.end.unwrap_or(0);
|
|
||||||
self.config.start = Some(end - n as i64);
|
|
||||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
|
||||||
}}
|
|
||||||
|
|
||||||
/// Set the start position.
|
|
||||||
pub fn from(mut self, start: i64) -> RangeBuilder<T> {{
|
|
||||||
self.config.start = Some(start);
|
|
||||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
|
||||||
}}
|
|
||||||
|
|
||||||
/// Execute the query and return parsed JSON data (from start of data to end).
|
|
||||||
pub fn json(self) -> Result<MetricData<T>> {{
|
|
||||||
self.config.get_json(None)
|
|
||||||
}}
|
|
||||||
|
|
||||||
/// Execute the query and return CSV data as a string.
|
|
||||||
pub fn csv(self) -> Result<String> {{
|
|
||||||
self.config.get_text(Some("csv"))
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
/// Final builder with range fully specified. Can only call `json()` or `csv()`.
|
|
||||||
pub struct RangeBuilder<T> {{
|
pub struct RangeBuilder<T> {{
|
||||||
config: EndpointConfig,
|
config: EndpointConfig,
|
||||||
_marker: std::marker::PhantomData<T>,
|
_marker: std::marker::PhantomData<T>,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
impl<T: DeserializeOwned> RangeBuilder<T> {{
|
impl<T: DeserializeOwned> RangeBuilder<T> {{
|
||||||
/// Execute the query and return parsed JSON data.
|
/// Fetch the range as parsed JSON.
|
||||||
pub fn json(self) -> Result<MetricData<T>> {{
|
pub fn fetch(self) -> Result<MetricData<T>> {{
|
||||||
self.config.get_json(None)
|
self.config.get_json(None)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/// Execute the query and return CSV data as a string.
|
/// Fetch the range as CSV string.
|
||||||
pub fn csv(self) -> Result<String> {{
|
pub fn fetch_csv(self) -> Result<String> {{
|
||||||
self.config.get_text(Some("csv"))
|
self.config.get_text(Some("csv"))
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ use std::fmt::Write;
|
|||||||
use brk_types::TreeNode;
|
use brk_types::TreeNode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax, child_type_name,
|
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax,
|
||||||
generate_leaf_field, generate_tree_node_field, get_node_fields, get_pattern_instance_base,
|
generate_leaf_field, generate_tree_node_field, prepare_tree_node, to_snake_case,
|
||||||
prepare_tree_node, to_snake_case,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Generate tree structs.
|
/// Generate tree structs.
|
||||||
@@ -39,25 +38,29 @@ fn generate_tree_node(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Generate struct definition
|
||||||
writeln!(output, "/// Metrics tree node.").unwrap();
|
writeln!(output, "/// Metrics tree node.").unwrap();
|
||||||
writeln!(output, "pub struct {} {{", name).unwrap();
|
writeln!(output, "pub struct {} {{", name).unwrap();
|
||||||
|
|
||||||
for ((field, child_fields), (child_name, _)) in
|
for child in &ctx.children {
|
||||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
let field_name = to_snake_case(child.name);
|
||||||
{
|
let type_annotation = if child.should_inline {
|
||||||
let field_name = to_snake_case(&field.name);
|
child.inline_type_name.clone()
|
||||||
let type_annotation = metadata.resolve_tree_field_type(
|
} else {
|
||||||
field,
|
metadata.resolve_tree_field_type(
|
||||||
child_fields.as_deref(),
|
&child.field,
|
||||||
name,
|
child.child_fields.as_deref(),
|
||||||
child_name,
|
name,
|
||||||
GenericSyntax::RUST,
|
child.name,
|
||||||
);
|
GenericSyntax::RUST,
|
||||||
|
)
|
||||||
|
};
|
||||||
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
|
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
writeln!(output, "}}\n").unwrap();
|
writeln!(output, "}}\n").unwrap();
|
||||||
|
|
||||||
|
// Generate impl block
|
||||||
writeln!(output, "impl {} {{", name).unwrap();
|
writeln!(output, "impl {} {{", name).unwrap();
|
||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
@@ -67,53 +70,40 @@ fn generate_tree_node(
|
|||||||
writeln!(output, " Self {{").unwrap();
|
writeln!(output, " Self {{").unwrap();
|
||||||
|
|
||||||
let syntax = RustSyntax;
|
let syntax = RustSyntax;
|
||||||
for ((field_info, child_fields), (child_name, child_node)) in
|
for child in &ctx.children {
|
||||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
let field_name = to_snake_case(child.name);
|
||||||
{
|
|
||||||
let field_name = to_snake_case(&field_info.name);
|
|
||||||
|
|
||||||
// Check if this is a pattern type and if it's parameterizable
|
if child.is_leaf {
|
||||||
let is_parameterizable = child_fields
|
if let TreeNode::Leaf(leaf) = child.node {
|
||||||
.as_ref()
|
generate_leaf_field(
|
||||||
.is_some_and(|cf| metadata.is_parameterizable_fields(cf));
|
output,
|
||||||
|
&syntax,
|
||||||
if metadata.is_pattern_type(&field_info.rust_type) && is_parameterizable {
|
"client.clone()",
|
||||||
// Parameterizable pattern: use pattern constructor with metric base
|
child.name,
|
||||||
let pattern_base = get_pattern_instance_base(child_node);
|
leaf,
|
||||||
generate_tree_node_field(
|
metadata,
|
||||||
output,
|
" ",
|
||||||
&syntax,
|
);
|
||||||
field_info,
|
}
|
||||||
metadata,
|
} else if child.should_inline {
|
||||||
" ",
|
// Inline struct
|
||||||
child_name,
|
let path_expr = syntax.path_expr("base_path", &format!("_{}", child.name));
|
||||||
Some(&pattern_base),
|
|
||||||
);
|
|
||||||
} else if child_fields.is_some() {
|
|
||||||
// Non-parameterizable pattern or regular branch: use inline struct
|
|
||||||
let child_struct = child_type_name(name, child_name);
|
|
||||||
let path_expr = syntax.path_expr("base_path", &format!("_{}", child_name));
|
|
||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
" {}: {}::new(client.clone(), {}),",
|
" {}: {}::new(client.clone(), {}),",
|
||||||
field_name, child_struct, path_expr
|
field_name, child.inline_type_name, path_expr
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
} else if let TreeNode::Leaf(leaf) = child_node {
|
} else {
|
||||||
// Leaf field - use shared helper
|
// Use pattern constructor
|
||||||
generate_leaf_field(
|
generate_tree_node_field(
|
||||||
output,
|
output,
|
||||||
&syntax,
|
&syntax,
|
||||||
"client.clone()",
|
&child.field,
|
||||||
child_name,
|
|
||||||
leaf,
|
|
||||||
metadata,
|
metadata,
|
||||||
" ",
|
" ",
|
||||||
);
|
child.name,
|
||||||
} else {
|
Some(&child.base_result.base),
|
||||||
panic!(
|
|
||||||
"Field '{}' is a leaf with no TreeNode::Leaf. This shouldn't happen.",
|
|
||||||
field_info.name
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,21 +112,17 @@ fn generate_tree_node(
|
|||||||
writeln!(output, " }}").unwrap();
|
writeln!(output, " }}").unwrap();
|
||||||
writeln!(output, "}}\n").unwrap();
|
writeln!(output, "}}\n").unwrap();
|
||||||
|
|
||||||
for (child_name, child_node) in ctx.children {
|
// Generate child structs
|
||||||
if let TreeNode::Branch(grandchildren) = child_node {
|
for child in &ctx.children {
|
||||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
if child.should_inline {
|
||||||
// Generate child struct if no pattern match OR pattern is not parameterizable
|
generate_tree_node(
|
||||||
if !metadata.is_parameterizable_fields(&child_fields) {
|
output,
|
||||||
let child_struct = child_type_name(name, child_name);
|
&child.inline_type_name,
|
||||||
generate_tree_node(
|
child.node,
|
||||||
output,
|
pattern_lookup,
|
||||||
&child_struct,
|
metadata,
|
||||||
child_node,
|
generated,
|
||||||
pattern_lookup,
|
);
|
||||||
metadata,
|
|
||||||
generated,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ brk_rpc = { workspace = true }
|
|||||||
brk_server = { workspace = true }
|
brk_server = { workspace = true }
|
||||||
clap = { version = "4.5.54", features = ["derive", "string"] }
|
clap = { version = "4.5.54", features = ["derive", "string"] }
|
||||||
color-eyre = { workspace = true }
|
color-eyre = { workspace = true }
|
||||||
importmap = "0.1.2"
|
importmap = "0.1.3"
|
||||||
# importmap = { path = "../../../importmap" }
|
# importmap = { path = "../../../importmap" }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
minreq = { workspace = true }
|
minreq = { workspace = true }
|
||||||
|
|||||||
+40
-27
@@ -17,6 +17,7 @@ use brk_mempool::Mempool;
|
|||||||
use brk_query::AsyncQuery;
|
use brk_query::AsyncQuery;
|
||||||
use brk_reader::Reader;
|
use brk_reader::Reader;
|
||||||
use brk_server::{Server, VERSION};
|
use brk_server::{Server, VERSION};
|
||||||
|
use importmap::ImportMap;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use vecdb::Exit;
|
use vecdb::Exit;
|
||||||
|
|
||||||
@@ -85,25 +86,28 @@ pub fn run() -> color_eyre::Result<()> {
|
|||||||
let data_path = config.brkdir();
|
let data_path = config.brkdir();
|
||||||
|
|
||||||
let future = async move {
|
let future = async move {
|
||||||
let bundle_path = if website.is_some() {
|
// Try to find local dev directories - check cwd and parent directories
|
||||||
// Try to find local dev directories - check cwd and parent directories
|
let find_dev_dirs = || -> Option<(PathBuf, PathBuf)> {
|
||||||
let find_dev_dirs = || -> Option<(PathBuf, PathBuf)> {
|
let mut dir = std::env::current_dir().ok()?;
|
||||||
let mut dir = std::env::current_dir().ok()?;
|
loop {
|
||||||
loop {
|
let websites = dir.join("websites");
|
||||||
let websites = dir.join("websites");
|
let modules = dir.join("modules");
|
||||||
let modules = dir.join("modules");
|
if websites.exists() && modules.exists() {
|
||||||
if websites.exists() && modules.exists() {
|
return Some((websites, modules));
|
||||||
return Some((websites, modules));
|
|
||||||
}
|
|
||||||
// Stop at workspace root (crates/ indicates we're there)
|
|
||||||
if dir.join("crates").exists() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
dir = dir.parent()?.to_path_buf();
|
|
||||||
}
|
}
|
||||||
};
|
// Stop at workspace root (crates/ indicates we're there)
|
||||||
|
if dir.join("crates").exists() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
dir = dir.parent()?.to_path_buf();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let websites_path = if let Some((websites, _modules)) = find_dev_dirs() {
|
let dev_dirs = find_dev_dirs();
|
||||||
|
let is_dev = dev_dirs.is_some();
|
||||||
|
|
||||||
|
let bundle_path = if website.is_some() {
|
||||||
|
let websites_path = if let Some((websites, _modules)) = dev_dirs {
|
||||||
websites
|
websites
|
||||||
} else {
|
} else {
|
||||||
let downloaded_brk_path = downloads_path.join(format!("brk-{VERSION}"));
|
let downloaded_brk_path = downloads_path.join(format!("brk-{VERSION}"));
|
||||||
@@ -133,19 +137,28 @@ pub fn run() -> color_eyre::Result<()> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate import map for cache busting
|
// Generate import map for cache busting (disabled in dev mode)
|
||||||
if let Some(ref path) = bundle_path {
|
if let Some(ref path) = bundle_path {
|
||||||
match importmap::ImportMap::scan(path, "") {
|
let map = if is_dev {
|
||||||
Ok(map) => {
|
ImportMap::empty()
|
||||||
let html_path = path.join("index.html");
|
} else {
|
||||||
if let Ok(html) = fs::read_to_string(&html_path)
|
match ImportMap::scan(path, "") {
|
||||||
&& let Some(updated) = map.update_html(&html)
|
Ok(map) => map,
|
||||||
{
|
Err(e) => {
|
||||||
let _ = fs::write(&html_path, updated);
|
tracing::error!("Failed to generate importmap: {e}");
|
||||||
info!("Updated importmap in index.html");
|
ImportMap::empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => tracing::error!("Failed to generate importmap: {e}"),
|
};
|
||||||
|
|
||||||
|
let html_path = path.join("index.html");
|
||||||
|
if let Ok(html) = fs::read_to_string(&html_path)
|
||||||
|
&& let Some(updated) = map.update_html(&html)
|
||||||
|
{
|
||||||
|
let _ = fs::write(&html_path, updated);
|
||||||
|
if !is_dev {
|
||||||
|
info!("Updated importmap in index.html");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ fn main() -> brk_client::Result<()> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fetch price data using the typed metrics API
|
// Fetch price data using the typed metrics API
|
||||||
|
// Using new idiomatic API: last(3).fetch()
|
||||||
let price_close = client
|
let price_close = client
|
||||||
.metrics()
|
.metrics()
|
||||||
.price
|
.price
|
||||||
@@ -22,8 +23,8 @@ fn main() -> brk_client::Result<()> {
|
|||||||
.close
|
.close
|
||||||
.by
|
.by
|
||||||
.dateindex()
|
.dateindex()
|
||||||
.from(-3)
|
.last(3)
|
||||||
.json()?;
|
.fetch()?;
|
||||||
println!("Last 3 price close values: {:?}", price_close);
|
println!("Last 3 price close values: {:?}", price_close);
|
||||||
|
|
||||||
// Fetch block data
|
// Fetch block data
|
||||||
@@ -35,12 +36,11 @@ fn main() -> brk_client::Result<()> {
|
|||||||
.sum
|
.sum
|
||||||
.by
|
.by
|
||||||
.dateindex()
|
.dateindex()
|
||||||
.from(-3)
|
.last(3)
|
||||||
.json()?;
|
.fetch()?;
|
||||||
println!("Last 3 block count values: {:?}", block_count);
|
println!("Last 3 block count values: {:?}", block_count);
|
||||||
|
|
||||||
// Fetch supply data
|
// Fetch supply data
|
||||||
//
|
|
||||||
dbg!(
|
dbg!(
|
||||||
client
|
client
|
||||||
.metrics()
|
.metrics()
|
||||||
@@ -58,8 +58,8 @@ fn main() -> brk_client::Result<()> {
|
|||||||
.bitcoin
|
.bitcoin
|
||||||
.by
|
.by
|
||||||
.dateindex()
|
.dateindex()
|
||||||
.from(-3)
|
.last(3)
|
||||||
.csv()?;
|
.fetch_csv()?;
|
||||||
println!("Last 3 circulating supply values: {:?}", circulating);
|
println!("Last 3 circulating supply values: {:?}", circulating);
|
||||||
|
|
||||||
// Using generic metric fetching
|
// Using generic metric fetching
|
||||||
|
|||||||
+1007
-2246
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
|||||||
use std::{env, path::Path, thread};
|
use std::{env, path::Path, thread, time::Instant};
|
||||||
|
|
||||||
use brk_computer::Computer;
|
use brk_computer::Computer;
|
||||||
use brk_error::Result;
|
use brk_error::Result;
|
||||||
use brk_fetcher::Fetcher;
|
use brk_fetcher::Fetcher;
|
||||||
use brk_indexer::Indexer;
|
use brk_indexer::Indexer;
|
||||||
use vecdb::{AnyStoredVec, Exit};
|
use vecdb::{AnySerializableVec, AnyVec, Exit};
|
||||||
|
|
||||||
pub fn main() -> Result<()> {
|
pub fn main() -> Result<()> {
|
||||||
// Can't increase main thread's stack size, thus we need to use another thread
|
// Can't increase main thread's stack size, thus we need to use another thread
|
||||||
@@ -19,7 +19,6 @@ fn run() -> Result<()> {
|
|||||||
brk_logger::init(None)?;
|
brk_logger::init(None)?;
|
||||||
|
|
||||||
let outputs_dir = Path::new(&env::var("HOME").unwrap()).join(".brk");
|
let outputs_dir = Path::new(&env::var("HOME").unwrap()).join(".brk");
|
||||||
// let outputs_dir = Path::new("../../_outputs");
|
|
||||||
|
|
||||||
let indexer = Indexer::forced_import(&outputs_dir)?;
|
let indexer = Indexer::forced_import(&outputs_dir)?;
|
||||||
|
|
||||||
@@ -30,7 +29,35 @@ fn run() -> Result<()> {
|
|||||||
|
|
||||||
let computer = Computer::forced_import(&outputs_dir, &indexer, Some(fetcher))?;
|
let computer = Computer::forced_import(&outputs_dir, &indexer, Some(fetcher))?;
|
||||||
|
|
||||||
let _a = dbg!(computer.transactions.fees.fee.base.region().meta());
|
// Test emptyaddressdata (underlying BytesVec) - direct access
|
||||||
|
let empty_data = &computer.distribution.addresses_data.empty;
|
||||||
|
println!("emptyaddressdata (BytesVec) len: {}", empty_data.len());
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
empty_data.write_json(Some(empty_data.len() - 1), Some(empty_data.len()), &mut buf)?;
|
||||||
|
println!("emptyaddressdata last item JSON: {}", String::from_utf8_lossy(&buf));
|
||||||
|
println!("Time for BytesVec write_json: {:?}", start.elapsed());
|
||||||
|
|
||||||
|
// Test emptyaddressindex (LazyVecFrom1 wrapper) - computed access
|
||||||
|
let empty_index = &computer.distribution.emptyaddressindex;
|
||||||
|
println!("\nemptyaddressindex (LazyVecFrom1) len: {}", empty_index.len());
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
empty_index.write_json(Some(empty_index.len() - 1), Some(empty_index.len()), &mut buf)?;
|
||||||
|
println!("emptyaddressindex last item JSON: {}", String::from_utf8_lossy(&buf));
|
||||||
|
println!("Time for LazyVecFrom1 write_json: {:?}", start.elapsed());
|
||||||
|
|
||||||
|
// Compare with loaded versions
|
||||||
|
let loaded_data = &computer.distribution.addresses_data.loaded;
|
||||||
|
println!("\nloadedaddressdata (BytesVec) len: {}", loaded_data.len());
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
loaded_data.write_json(Some(loaded_data.len() - 1), Some(loaded_data.len()), &mut buf)?;
|
||||||
|
println!("loadedaddressdata last item JSON: {}", String::from_utf8_lossy(&buf));
|
||||||
|
println!("Time for BytesVec write_json: {:?}", start.elapsed());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,18 @@ pub enum Error {
|
|||||||
|
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
|
/// Returns true if this error is due to a file lock (another process has the database open).
|
||||||
|
/// Lock errors are transient and should not trigger data deletion.
|
||||||
|
pub fn is_lock_error(&self) -> bool {
|
||||||
|
matches!(self, Error::VecDB(e) if e.is_lock_error())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this error indicates data corruption or version incompatibility.
|
||||||
|
/// These errors may require resetting/deleting the data to recover.
|
||||||
|
pub fn is_data_error(&self) -> bool {
|
||||||
|
matches!(self, Error::VecDB(e) if e.is_data_error())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true if this network/fetch error indicates a permanent/blocking condition
|
/// Returns true if this network/fetch error indicates a permanent/blocking condition
|
||||||
/// that won't be resolved by retrying (e.g., DNS failure, connection refused, blocked endpoint).
|
/// that won't be resolved by retrying (e.g., DNS failure, connection refused, blocked endpoint).
|
||||||
/// Returns false for transient errors worth retrying (timeouts, rate limits, server errors).
|
/// Returns false for transient errors worth retrying (timeouts, rate limits, server errors).
|
||||||
|
|||||||
@@ -47,27 +47,26 @@ impl Indexer {
|
|||||||
let indexed_path = outputs_dir.join("indexed");
|
let indexed_path = outputs_dir.join("indexed");
|
||||||
|
|
||||||
let try_import = || -> Result<Self> {
|
let try_import = || -> Result<Self> {
|
||||||
let (vecs, stores) = thread::scope(|s| -> Result<_> {
|
let i = Instant::now();
|
||||||
let vecs = s.spawn(|| -> Result<_> {
|
let vecs = Vecs::forced_import(&indexed_path, VERSION)?;
|
||||||
let i = Instant::now();
|
info!("Imported vecs in {:?}", i.elapsed());
|
||||||
let vecs = Vecs::forced_import(&indexed_path, VERSION)?;
|
|
||||||
info!("Imported vecs in {:?}", i.elapsed());
|
|
||||||
Ok(vecs)
|
|
||||||
});
|
|
||||||
|
|
||||||
let i = Instant::now();
|
let i = Instant::now();
|
||||||
let stores = Stores::forced_import(&indexed_path, VERSION)?;
|
let stores = Stores::forced_import(&indexed_path, VERSION)?;
|
||||||
info!("Imported stores in {:?}", i.elapsed());
|
info!("Imported stores in {:?}", i.elapsed());
|
||||||
|
|
||||||
Ok((vecs.join().unwrap()?, stores))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Self { vecs, stores })
|
Ok(Self { vecs, stores })
|
||||||
};
|
};
|
||||||
|
|
||||||
match try_import() {
|
match try_import() {
|
||||||
Ok(result) => Ok(result),
|
Ok(result) => Ok(result),
|
||||||
Err(err) if can_retry => {
|
Err(err) if err.is_lock_error() => {
|
||||||
|
// Lock errors are transient - another process has the database open.
|
||||||
|
// Don't delete data, just return the error.
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
Err(err) if can_retry && err.is_data_error() => {
|
||||||
|
// Data corruption or version mismatch - safe to delete and retry
|
||||||
info!("{err:?}, deleting {indexed_path:?} and retrying");
|
info!("{err:?}, deleting {indexed_path:?} and retrying");
|
||||||
fs::remove_dir_all(&indexed_path)?;
|
fs::remove_dir_all(&indexed_path)?;
|
||||||
Self::forced_import_inner(outputs_dir, false)
|
Self::forced_import_inner(outputs_dir, false)
|
||||||
|
|||||||
+2564
-3255
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,10 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/bitcoinresearchkit/brk/issues"
|
"url": "https://github.com/bitcoinresearchkit/brk/issues"
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "node tests/basic.js",
|
||||||
|
"test:tree": "node tests/tree.js"
|
||||||
|
},
|
||||||
"description": "BRK JavaScript client",
|
"description": "BRK JavaScript client",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|||||||
@@ -1,5 +1,74 @@
|
|||||||
import { BrkClient } from "../index.js";
|
import { BrkClient } from "../index.js";
|
||||||
|
|
||||||
let client = new BrkClient("http://localhost:3110");
|
const client = new BrkClient("http://localhost:3110");
|
||||||
|
|
||||||
let blocks = await client.getBlocks();
|
console.log("Testing idiomatic API...\n");
|
||||||
|
|
||||||
|
// Test getter access (property)
|
||||||
|
console.log("1. Getter access (.by.dateindex):");
|
||||||
|
const all = await client.metrics.price.usd.split.close.by.dateindex;
|
||||||
|
console.log(` Total: ${all.total}, Got: ${all.data.length} items\n`);
|
||||||
|
|
||||||
|
// Test dynamic access (bracket notation)
|
||||||
|
console.log("2. Dynamic access (.by['dateindex']):");
|
||||||
|
const allDynamic = await client.metrics.price.usd.split.close.by["dateindex"];
|
||||||
|
console.log(
|
||||||
|
` Total: ${allDynamic.total}, Got: ${allDynamic.data.length} items\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test fetch all (explicit .fetch())
|
||||||
|
console.log("3. Explicit .fetch():");
|
||||||
|
const allExplicit =
|
||||||
|
await client.metrics.price.usd.split.close.by.dateindex.fetch();
|
||||||
|
console.log(
|
||||||
|
` Total: ${allExplicit.total}, Got: ${allExplicit.data.length} items\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test first(n)
|
||||||
|
console.log("4. First 5 items (.first(5)):");
|
||||||
|
const first5 = await client.metrics.price.usd.split.close.by.dateindex.first(5);
|
||||||
|
console.log(
|
||||||
|
` Total: ${first5.total}, Start: ${first5.start}, End: ${first5.end}, Got: ${first5.data.length} items\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test last(n)
|
||||||
|
console.log("5. Last 5 items (.last(5)):");
|
||||||
|
const last5 = await client.metrics.price.usd.split.close.by.dateindex.last(5);
|
||||||
|
console.log(
|
||||||
|
` Total: ${last5.total}, Start: ${last5.start}, End: ${last5.end}, Got: ${last5.data.length} items\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test slice(start, end)
|
||||||
|
console.log("6. Slice 10-20 (.slice(10, 20)):");
|
||||||
|
const sliced = await client.metrics.price.usd.split.close.by.dateindex.slice(
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` Total: ${sliced.total}, Start: ${sliced.start}, End: ${sliced.end}, Got: ${sliced.data.length} items\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test get(index) - single item
|
||||||
|
console.log("7. Single item (.get(100)):");
|
||||||
|
const single = await client.metrics.price.usd.split.close.by.dateindex.get(100);
|
||||||
|
console.log(
|
||||||
|
` Total: ${single.total}, Start: ${single.start}, End: ${single.end}, Got: ${single.data.length} item(s)\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test skip(n).take(m) chaining
|
||||||
|
console.log("8. Skip and take (.skip(100).take(10)):");
|
||||||
|
const skipTake = await client.metrics.price.usd.split.close.by.dateindex
|
||||||
|
.skip(100)
|
||||||
|
.take(10);
|
||||||
|
console.log(
|
||||||
|
` Total: ${skipTake.total}, Start: ${skipTake.start}, End: ${skipTake.end}, Got: ${skipTake.data.length} items\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test fetchCsv
|
||||||
|
console.log("9. Fetch as CSV (.last(3).fetchCsv()):");
|
||||||
|
const csv = await client.metrics.price.usd.split.close.by.dateindex
|
||||||
|
.last(3)
|
||||||
|
.fetchCsv();
|
||||||
|
console.log(` CSV preview: ${csv.substring(0, 100)}...\n`);
|
||||||
|
|
||||||
|
console.log("All tests passed!");
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { BrkClient } from "../index.js";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively collect all metric patterns from the tree.
|
* Recursively collect all metric patterns from the tree.
|
||||||
* @param {object} obj
|
* @param {Record<string, any>} obj
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
* @returns {Array<{path: string, metric: object, indexes: string[]}>}
|
* @returns {Array<{path: string, metric: Record<string, any>, indexes: string[]}>}
|
||||||
*/
|
*/
|
||||||
function getAllMetrics(obj, path = "") {
|
function getAllMetrics(obj, path = "") {
|
||||||
const metrics = [];
|
const metrics = [];
|
||||||
@@ -21,10 +21,10 @@ function getAllMetrics(obj, path = "") {
|
|||||||
|
|
||||||
const currentPath = path ? `${path}.${key}` : key;
|
const currentPath = path ? `${path}.${key}` : key;
|
||||||
|
|
||||||
// Check if this is a metric pattern (has 'by' property with index methods)
|
// Check if this is a metric pattern (has 'by' property with index getters)
|
||||||
if (attr.by && typeof attr.by === "object") {
|
if (attr.by && typeof attr.by === "object") {
|
||||||
const indexes = Object.keys(attr.by).filter(
|
const indexes = Object.keys(attr.by).filter(
|
||||||
(k) => !k.startsWith("_") && typeof attr.by[k] === "function",
|
(k) => !k.startsWith("_") && typeof attr.by[k] === "object",
|
||||||
);
|
);
|
||||||
if (indexes.length > 0) {
|
if (indexes.length > 0) {
|
||||||
metrics.push({ path: currentPath, metric: attr, indexes });
|
metrics.push({ path: currentPath, metric: attr, indexes });
|
||||||
@@ -41,54 +41,38 @@ function getAllMetrics(obj, path = "") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function testAllEndpoints() {
|
async function testAllEndpoints() {
|
||||||
const client = new BrkClient("http://localhost:3110");
|
const client = new BrkClient({ baseUrl: "http://localhost:3110", timeout: 15000 });
|
||||||
|
|
||||||
const metrics = getAllMetrics(client.tree);
|
const metrics = getAllMetrics(client.metrics);
|
||||||
console.log(`\nFound ${metrics.length} metrics`);
|
console.log(`\nFound ${metrics.length} metrics`);
|
||||||
|
|
||||||
let success = 0;
|
let success = 0;
|
||||||
let failed = 0;
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
for (const { path, metric, indexes } of metrics) {
|
for (const { path, metric, indexes } of metrics) {
|
||||||
for (const idxName of indexes) {
|
for (const idxName of indexes) {
|
||||||
try {
|
try {
|
||||||
const endpoint = metric.by[idxName]();
|
const endpoint = metric.by[idxName];
|
||||||
const res = await endpoint.range(-3);
|
const res = await endpoint.last(1);
|
||||||
const count = res.data.length;
|
const count = res.data.length;
|
||||||
if (count !== 3) {
|
if (count !== 1) {
|
||||||
failed++;
|
console.log(
|
||||||
const errorMsg = `FAIL: ${path}.by.${idxName}() -> expected 3, got ${count}`;
|
`FAIL: ${path}.by.${idxName} -> expected 1, got ${count}`,
|
||||||
errors.push(errorMsg);
|
);
|
||||||
console.log(errorMsg);
|
return;
|
||||||
} else {
|
|
||||||
success++;
|
|
||||||
console.log(`OK: ${path}.by.${idxName}() -> ${count} items`);
|
|
||||||
}
|
}
|
||||||
|
success++;
|
||||||
|
console.log(`OK: ${path}.by.${idxName} -> ${count} items`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
failed++;
|
console.log(
|
||||||
const errorMsg = `FAIL: ${path}.by.${idxName}() -> ${e.message}`;
|
`FAIL: ${path}.by.${idxName} -> ${e instanceof Error ? e.message : e}`,
|
||||||
errors.push(errorMsg);
|
);
|
||||||
console.log(errorMsg);
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n=== Results ===`);
|
console.log(`\n=== Results ===`);
|
||||||
console.log(`Success: ${success}`);
|
console.log(`Success: ${success}`);
|
||||||
console.log(`Failed: ${failed}`);
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
console.log(`\nErrors:`);
|
|
||||||
errors.slice(0, 10).forEach((err) => console.log(` ${err}`));
|
|
||||||
if (errors.length > 10) {
|
|
||||||
console.log(` ... and ${errors.length - 10} more`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failed > 0) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testAllEndpoints();
|
testAllEndpoints();
|
||||||
|
|||||||
+2739
-4694
File diff suppressed because it is too large
Load Diff
@@ -36,29 +36,30 @@ def test_fetch_csv_metric():
|
|||||||
|
|
||||||
def test_fetch_typed_metric():
|
def test_fetch_typed_metric():
|
||||||
client = BrkClient("http://localhost:3110")
|
client = BrkClient("http://localhost:3110")
|
||||||
a = client.metrics.constants.constant_0.by.dateindex().from_(-10).json()
|
# Using new idiomatic API: tail(10).fetch() or [-10:].fetch()
|
||||||
|
a = client.metrics.constants.constant_0.by.dateindex().tail(10).fetch()
|
||||||
print(a)
|
print(a)
|
||||||
b = client.metrics.outputs.count.utxo_count.by.height().from_(-10).json()
|
b = client.metrics.outputs.count.utxo_count.by.height().tail(10).fetch()
|
||||||
print(b)
|
print(b)
|
||||||
c = client.metrics.price.usd.split.close.by.dateindex().from_(-10).json()
|
c = client.metrics.price.usd.split.close.by.dateindex().tail(10).fetch()
|
||||||
print(c)
|
print(c)
|
||||||
d = (
|
d = (
|
||||||
client.metrics.market.dca.period_lump_sum_stack._10y.dollars.by.dateindex()
|
client.metrics.market.dca.period_lump_sum_stack._10y.dollars.by.dateindex()
|
||||||
.from_(-10)
|
.tail(10)
|
||||||
.json()
|
.fetch()
|
||||||
)
|
)
|
||||||
print(d)
|
print(d)
|
||||||
e = (
|
e = (
|
||||||
client.metrics.market.dca.class_average_price._2017.by.dateindex()
|
client.metrics.market.dca.class_average_price._2017.by.dateindex()
|
||||||
.from_(-10)
|
.tail(10)
|
||||||
.json()
|
.fetch()
|
||||||
)
|
)
|
||||||
print(e)
|
print(e)
|
||||||
f = (
|
f = (
|
||||||
client.metrics.distribution.address_cohorts.amount_range._10k_sats_to_100k_sats.activity.sent.dollars.cumulative.by.dateindex()
|
client.metrics.distribution.address_cohorts.amount_range._10k_sats_to_100k_sats.activity.sent.dollars.cumulative.by.dateindex()
|
||||||
.from_(-10)
|
.tail(10)
|
||||||
.json()
|
.fetch()
|
||||||
)
|
)
|
||||||
print(f)
|
print(f)
|
||||||
g = client.metrics.price.usd.ohlc.by.dateindex().from_(-10).json()
|
g = client.metrics.price.usd.ohlc.by.dateindex().tail(10).fetch()
|
||||||
print(g)
|
print(g)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ def test_all_endpoints():
|
|||||||
"""Test fetching last 3 values from all metric endpoints."""
|
"""Test fetching last 3 values from all metric endpoints."""
|
||||||
client = BrkClient("http://localhost:3110")
|
client = BrkClient("http://localhost:3110")
|
||||||
|
|
||||||
metrics = get_all_metrics(client.tree)
|
metrics = get_all_metrics(client.metrics)
|
||||||
print(f"\nFound {len(metrics)} metrics")
|
print(f"\nFound {len(metrics)} metrics")
|
||||||
|
|
||||||
success = 0
|
success = 0
|
||||||
@@ -53,7 +53,8 @@ def test_all_endpoints():
|
|||||||
try:
|
try:
|
||||||
by = metric.by
|
by = metric.by
|
||||||
endpoint = getattr(by, idx_name)()
|
endpoint = getattr(by, idx_name)()
|
||||||
res = endpoint.range(-3)
|
# Use the new idiomatic API: tail(3).fetch() or [-3:].fetch()
|
||||||
|
res = endpoint.tail(3).fetch()
|
||||||
count = len(res["data"])
|
count = len(res["data"])
|
||||||
if count != 3:
|
if count != 3:
|
||||||
failed += 1
|
failed += 1
|
||||||
|
|||||||
+1
-142
@@ -1515,148 +1515,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- IMPORTMAP -->
|
<!-- IMPORTMAP -->
|
||||||
<script type="importmap">
|
|
||||||
{
|
|
||||||
"imports": {
|
|
||||||
"/scripts/chart/index.js": "/scripts/chart/index.024e5d6b.js",
|
|
||||||
"/scripts/chart/oklch.js": "/scripts/chart/oklch.21450255.js",
|
|
||||||
"/scripts/entry.js": "/scripts/entry.93446a1b.js",
|
|
||||||
"/scripts/lazy.js": "/scripts/lazy.1ae52534.js",
|
|
||||||
"/scripts/main.js": "/scripts/main.22a5bd79.js",
|
|
||||||
"/scripts/modules/brk-client/index.js": "/scripts/modules/brk-client/index.664c39c8.js",
|
|
||||||
"/scripts/modules/brk-client/tests/basic.js": "/scripts/modules/brk-client/tests/basic.b92ff866.js",
|
|
||||||
"/scripts/modules/brk-client/tests/tree.js": "/scripts/modules/brk-client/tests/tree.ba9474f7.js",
|
|
||||||
"/scripts/modules/lean-qr/2.6.1/index.mjs": "/scripts/modules/lean-qr/2.6.1/index.09195c13.mjs",
|
|
||||||
"/scripts/modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs": "/scripts/modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.803b7fb0.mjs",
|
|
||||||
"/scripts/modules/lightweight-charts/5.0.9/dist/lightweight-charts.standalone.production.mjs": "/scripts/modules/lightweight-charts/5.0.9/dist/lightweight-charts.standalone.production.1e264451.mjs",
|
|
||||||
"/scripts/modules/lightweight-charts/5.1.0/dist/lightweight-charts.standalone.production.mjs": "/scripts/modules/lightweight-charts/5.1.0/dist/lightweight-charts.standalone.production.5c2a821a.mjs",
|
|
||||||
"/scripts/modules/modern-screenshot/4.6.6/dist/index.mjs": "/scripts/modules/modern-screenshot/4.6.6/dist/index.0f951334.mjs",
|
|
||||||
"/scripts/modules/modern-screenshot/4.6.7/dist/index.js": "/scripts/modules/modern-screenshot/4.6.7/dist/index.070574d6.js",
|
|
||||||
"/scripts/modules/modern-screenshot/4.6.7/dist/index.mjs": "/scripts/modules/modern-screenshot/4.6.7/dist/index.e9e389fe.mjs",
|
|
||||||
"/scripts/modules/modern-screenshot/4.6.7/dist/worker.js": "/scripts/modules/modern-screenshot/4.6.7/dist/worker.1265d9cd.js",
|
|
||||||
"/scripts/modules/solidjs-signals/0.6.3/dist/prod.js": "/scripts/modules/solidjs-signals/0.6.3/dist/prod.2f80e335.js",
|
|
||||||
"/scripts/modules/solidjs-signals/0.8.5/dist/prod.js": "/scripts/modules/solidjs-signals/0.8.5/dist/prod.8ae56250.js",
|
|
||||||
"/scripts/options/_partial_old.js": "/scripts/options/_partial_old.62bf3faa.js",
|
|
||||||
"/scripts/options/chain.js": "/scripts/options/chain.c173ace8.js",
|
|
||||||
"/scripts/options/cohorts/address.js": "/scripts/options/cohorts/address.2e8a42cb.js",
|
|
||||||
"/scripts/options/cohorts/data.js": "/scripts/options/cohorts/data.11381d83.js",
|
|
||||||
"/scripts/options/cohorts/index.js": "/scripts/options/cohorts/index.b0e57c9d.js",
|
|
||||||
"/scripts/options/cohorts/shared.js": "/scripts/options/cohorts/shared.87b9837c.js",
|
|
||||||
"/scripts/options/cohorts/utxo.js": "/scripts/options/cohorts/utxo.f0e69857.js",
|
|
||||||
"/scripts/options/cointime.js": "/scripts/options/cointime.e25633d9.js",
|
|
||||||
"/scripts/options/colors/cohorts.js": "/scripts/options/colors/cohorts.262d4551.js",
|
|
||||||
"/scripts/options/colors/index.js": "/scripts/options/colors/index.a54dc83f.js",
|
|
||||||
"/scripts/options/colors/misc.js": "/scripts/options/colors/misc.bee7dbee.js",
|
|
||||||
"/scripts/options/constants.js": "/scripts/options/constants.16dfce27.js",
|
|
||||||
"/scripts/options/context.js": "/scripts/options/context.8bf2932e.js",
|
|
||||||
"/scripts/options/full.js": "/scripts/options/full.11772605.js",
|
|
||||||
"/scripts/options/market/averages.js": "/scripts/options/market/averages.d95aa3e1.js",
|
|
||||||
"/scripts/options/market/index.js": "/scripts/options/market/index.e3b750d6.js",
|
|
||||||
"/scripts/options/market/indicators/bands.js": "/scripts/options/market/indicators/bands.81b19b83.js",
|
|
||||||
"/scripts/options/market/indicators/index.js": "/scripts/options/market/indicators/index.70e9b3e4.js",
|
|
||||||
"/scripts/options/market/indicators/momentum.js": "/scripts/options/market/indicators/momentum.48e71442.js",
|
|
||||||
"/scripts/options/market/indicators/onchain.js": "/scripts/options/market/indicators/onchain.f75ddfd9.js",
|
|
||||||
"/scripts/options/market/indicators/volatility.js": "/scripts/options/market/indicators/volatility.08ec92d7.js",
|
|
||||||
"/scripts/options/market/investing.js": "/scripts/options/market/investing.57ded805.js",
|
|
||||||
"/scripts/options/market/performance.js": "/scripts/options/market/performance.36c7ad40.js",
|
|
||||||
"/scripts/options/market/utils.js": "/scripts/options/market/utils.e3e058d8.js",
|
|
||||||
"/scripts/options/partial.js": "/scripts/options/partial.ab8fdf12.js",
|
|
||||||
"/scripts/options/series.js": "/scripts/options/series.5a2a34ed.js",
|
|
||||||
"/scripts/options/types.js": "/scripts/options/types.64db5149.js",
|
|
||||||
"/scripts/options/unused.js": "/scripts/options/unused.24a71427.js",
|
|
||||||
"/scripts/panes/chart/index.js": "/scripts/panes/chart/index.947ceee8.js",
|
|
||||||
"/scripts/panes/chart/screenshot.js": "/scripts/panes/chart/screenshot.adc8da89.js",
|
|
||||||
"/scripts/panes/explorer.js": "/scripts/panes/explorer.91a5a9ae.js",
|
|
||||||
"/scripts/panes/nav.js": "/scripts/panes/nav.0338dc4b.js",
|
|
||||||
"/scripts/panes/search.js": "/scripts/panes/search.0338dc4b.js",
|
|
||||||
"/scripts/panes/simulation.js": "/scripts/panes/simulation.abf9ee5d.js",
|
|
||||||
"/scripts/panes/table.js": "/scripts/panes/table.00486691.js",
|
|
||||||
"/scripts/resources.js": "/scripts/resources.77bdf76f.js",
|
|
||||||
"/scripts/signals.js": "/scripts/signals.2ba0669e.js",
|
|
||||||
"/scripts/utils/array.js": "/scripts/utils/array.1863f57c.js",
|
|
||||||
"/scripts/utils/colors.js": "/scripts/utils/colors.c95b2e03.js",
|
|
||||||
"/scripts/utils/date.js": "/scripts/utils/date.12ad717d.js",
|
|
||||||
"/scripts/utils/dom.js": "/scripts/utils/dom.4d99f37f.js",
|
|
||||||
"/scripts/utils/elements.js": "/scripts/utils/elements.6fe024ed.js",
|
|
||||||
"/scripts/utils/env.js": "/scripts/utils/env.594127e7.js",
|
|
||||||
"/scripts/utils/format.js": "/scripts/utils/format.4bdbfe40.js",
|
|
||||||
"/scripts/utils/serde.js": "/scripts/utils/serde.f9c7ed2b.js",
|
|
||||||
"/scripts/utils/storage.js": "/scripts/utils/storage.cbd2ff9c.js",
|
|
||||||
"/scripts/utils/timing.js": "/scripts/utils/timing.ae6a47b8.js",
|
|
||||||
"/scripts/utils/units.js": "/scripts/utils/units.30278ea7.js",
|
|
||||||
"/scripts/utils/url.js": "/scripts/utils/url.20469bf9.js",
|
|
||||||
"/scripts/utils/ws.js": "/scripts/utils/ws.fe3fb4b1.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<link rel="modulepreload" href="/scripts/chart/index.024e5d6b.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/chart/oklch.21450255.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/entry.93446a1b.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/lazy.1ae52534.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/main.22a5bd79.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/modules/brk-client/index.664c39c8.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/modules/brk-client/tests/basic.b92ff866.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/modules/brk-client/tests/tree.ba9474f7.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/modules/lean-qr/2.6.1/index.09195c13.mjs">
|
|
||||||
<link rel="modulepreload" href="/scripts/modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.803b7fb0.mjs">
|
|
||||||
<link rel="modulepreload" href="/scripts/modules/lightweight-charts/5.0.9/dist/lightweight-charts.standalone.production.1e264451.mjs">
|
|
||||||
<link rel="modulepreload" href="/scripts/modules/lightweight-charts/5.1.0/dist/lightweight-charts.standalone.production.5c2a821a.mjs">
|
|
||||||
<link rel="modulepreload" href="/scripts/modules/modern-screenshot/4.6.6/dist/index.0f951334.mjs">
|
|
||||||
<link rel="modulepreload" href="/scripts/modules/modern-screenshot/4.6.7/dist/index.070574d6.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/modules/modern-screenshot/4.6.7/dist/index.e9e389fe.mjs">
|
|
||||||
<link rel="modulepreload" href="/scripts/modules/modern-screenshot/4.6.7/dist/worker.1265d9cd.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/modules/solidjs-signals/0.6.3/dist/prod.2f80e335.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/modules/solidjs-signals/0.8.5/dist/prod.8ae56250.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/_partial_old.62bf3faa.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/chain.c173ace8.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/cohorts/address.2e8a42cb.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/cohorts/data.11381d83.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/cohorts/index.b0e57c9d.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/cohorts/shared.87b9837c.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/cohorts/utxo.f0e69857.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/cointime.e25633d9.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/colors/cohorts.262d4551.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/colors/index.a54dc83f.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/colors/misc.bee7dbee.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/constants.16dfce27.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/context.8bf2932e.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/full.11772605.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/market/averages.d95aa3e1.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/market/index.e3b750d6.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/market/indicators/bands.81b19b83.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/market/indicators/index.70e9b3e4.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/market/indicators/momentum.48e71442.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/market/indicators/onchain.f75ddfd9.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/market/indicators/volatility.08ec92d7.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/market/investing.57ded805.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/market/performance.36c7ad40.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/market/utils.e3e058d8.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/partial.ab8fdf12.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/series.5a2a34ed.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/types.64db5149.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/options/unused.24a71427.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/panes/chart/index.947ceee8.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/panes/chart/screenshot.adc8da89.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/panes/explorer.91a5a9ae.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/panes/nav.0338dc4b.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/panes/search.0338dc4b.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/panes/simulation.abf9ee5d.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/panes/table.00486691.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/resources.77bdf76f.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/signals.2ba0669e.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/utils/array.1863f57c.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/utils/colors.c95b2e03.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/utils/date.12ad717d.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/utils/dom.4d99f37f.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/utils/elements.6fe024ed.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/utils/env.594127e7.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/utils/format.4bdbfe40.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/utils/serde.f9c7ed2b.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/utils/storage.cbd2ff9c.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/utils/timing.ae6a47b8.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/utils/units.30278ea7.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/utils/url.20469bf9.js">
|
|
||||||
<link rel="modulepreload" href="/scripts/utils/ws.fe3fb4b1.js">
|
|
||||||
<!-- /IMPORTMAP -->
|
<!-- /IMPORTMAP -->
|
||||||
|
|
||||||
<!-- ------- -->
|
<!-- ------- -->
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { throttle } from "../utils/timing.js";
|
|||||||
import { serdeBool } from "../utils/serde.js";
|
import { serdeBool } from "../utils/serde.js";
|
||||||
import { stringToId } from "../utils/format.js";
|
import { stringToId } from "../utils/format.js";
|
||||||
import { style } from "../utils/elements.js";
|
import { style } from "../utils/elements.js";
|
||||||
|
import { resources } from "../resources.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Valued
|
* @typedef {Object} Valued
|
||||||
@@ -70,20 +71,18 @@ const lineWidth = /** @type {any} */ (1.5);
|
|||||||
* @param {HTMLElement} args.parent
|
* @param {HTMLElement} args.parent
|
||||||
* @param {Signals} args.signals
|
* @param {Signals} args.signals
|
||||||
* @param {Colors} args.colors
|
* @param {Colors} args.colors
|
||||||
* @param {Resources} args.resources
|
|
||||||
* @param {BrkClient} args.brk
|
* @param {BrkClient} args.brk
|
||||||
* @param {Accessor<ChartableIndex>} args.index
|
* @param {Accessor<ChartableIndex>} args.index
|
||||||
* @param {((unknownTimeScaleCallback: VoidFunction) => void)} [args.timeScaleSetCallback]
|
* @param {((unknownTimeScaleCallback: VoidFunction) => void)} [args.timeScaleSetCallback]
|
||||||
* @param {true} [args.fitContent]
|
* @param {true} [args.fitContent]
|
||||||
* @param {{unit: Unit; blueprints: AnySeriesBlueprint[]}[]} [args.config]
|
* @param {{unit: Unit; blueprints: AnySeriesBlueprint[]}[]} [args.config]
|
||||||
*/
|
*/
|
||||||
function createChartElement({
|
export function createChartElement({
|
||||||
parent,
|
parent,
|
||||||
signals,
|
signals,
|
||||||
colors,
|
colors,
|
||||||
id: chartId,
|
id: chartId,
|
||||||
index,
|
index,
|
||||||
resources,
|
|
||||||
brk,
|
brk,
|
||||||
timeScaleSetCallback,
|
timeScaleSetCallback,
|
||||||
fitContent,
|
fitContent,
|
||||||
@@ -1076,5 +1075,3 @@ function numberToUSFormat(value, digits, options) {
|
|||||||
* @typedef {typeof createChartElement} CreateChartElement
|
* @typedef {typeof createChartElement} CreateChartElement
|
||||||
* @typedef {ReturnType<createChartElement>} Chart
|
* @typedef {ReturnType<createChartElement>} Chart
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default { createChartElement };
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* @import { Signal, Signals, Accessor } from "./signals.js";
|
* @import { Signal, Signals, Accessor } from "./signals.js";
|
||||||
*
|
*
|
||||||
* @import * as Brk from "./modules/brk-client/index.js"
|
* @import * as Brk from "./modules/brk-client/index.js"
|
||||||
* @import { BrkClient} from "./modules/brk-client/index.js"
|
* @import { BrkClient, Index, Metric, MetricData } from "./modules/brk-client/index.js"
|
||||||
*
|
*
|
||||||
* @import { Resources, MetricResource } from './resources.js'
|
* @import { Resources, MetricResource } from './resources.js'
|
||||||
*
|
*
|
||||||
@@ -28,23 +28,91 @@
|
|||||||
// import uFuzzy = require("./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.d.ts");
|
// import uFuzzy = require("./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.d.ts");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {typeof import("./lazy")["default"]} Modules
|
|
||||||
* @typedef {[number, number, number, number]} OHLCTuple
|
* @typedef {[number, number, number, number]} OHLCTuple
|
||||||
|
*
|
||||||
|
* Brk type aliases
|
||||||
|
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts} UtxoCohortTree
|
||||||
|
* @typedef {Brk.MetricsTree_Distribution_AddressCohorts} AddressCohortTree
|
||||||
|
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_All} AllUtxoPattern
|
||||||
|
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_Term_Short} ShortTermPattern
|
||||||
|
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_Term_Long} LongTermPattern
|
||||||
|
* @typedef {Brk._10yPattern} MaxAgePattern
|
||||||
|
* @typedef {Brk._10yTo12yPattern} AgeRangePattern
|
||||||
|
* @typedef {Brk._0satsPattern2} UtxoAmountPattern
|
||||||
|
* @typedef {Brk._0satsPattern} AddressAmountPattern
|
||||||
|
* @typedef {Brk._100btcPattern} BasicUtxoPattern
|
||||||
|
* @typedef {Brk._0satsPattern2} EpochPattern
|
||||||
|
* @typedef {Brk.Ratio1ySdPattern} Ratio1ySdPattern
|
||||||
|
* @typedef {Brk.Dollars} Dollars
|
||||||
|
* @typedef {Brk.Price111dSmaPattern} EmaRatioPattern
|
||||||
|
* @typedef {Brk.CoinbasePattern} CoinbasePattern
|
||||||
|
* @typedef {Brk.ActivePriceRatioPattern} ActivePriceRatioPattern
|
||||||
|
* @typedef {Brk.UnclaimedRewardsPattern} ValuePattern
|
||||||
|
* @typedef {Brk.AnyMetricPattern} AnyMetricPattern
|
||||||
|
* @typedef {Brk.AnyMetricEndpointBuilder} AnyMetricEndpoint
|
||||||
|
* @typedef {Brk.AnyMetricData} AnyMetricData
|
||||||
|
* @typedef {Brk.AddrCountPattern} AddrCountPattern
|
||||||
|
* @typedef {Brk.MetricsTree_Blocks_Interval} IntervalPattern
|
||||||
|
* @typedef {Brk.MetricsTree_Supply_Circulating} SupplyPattern
|
||||||
|
* @typedef {Brk.RelativePattern} GlobalRelativePattern
|
||||||
|
* @typedef {Brk.RelativePattern2} OwnRelativePattern
|
||||||
|
* @typedef {Brk.RelativePattern5} FullRelativePattern
|
||||||
|
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_All_Relative} AllRelativePattern
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @typedef {Brk.BlockCountPattern<T>} BlockCountPattern
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @typedef {Brk.FullnessPattern<T>} FullnessPattern
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @typedef {Brk.FeeRatePattern<T>} FeeRatePattern
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @typedef {Brk.MetricEndpointBuilder<T>} MetricEndpoint
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @typedef {Brk.DollarsPattern<T>} SizePattern
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @typedef {Brk.CountPattern2<T>} CountStatsPattern
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @typedef {Brk.MetricsTree_Blocks_Size} BlockSizePattern
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Stats pattern union - accepts both CountStatsPattern and BlockSizePattern
|
||||||
|
* @typedef {CountStatsPattern<any> | BlockSizePattern} AnyStatsPattern
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
*
|
*
|
||||||
* @typedef {InstanceType<typeof BrkClient>["INDEXES"]} Indexes
|
* @typedef {InstanceType<typeof BrkClient>["INDEXES"]} Indexes
|
||||||
* @typedef {Indexes[number]} IndexName
|
* @typedef {Indexes[number]} IndexName
|
||||||
* @typedef {InstanceType<typeof BrkClient>["POOL_ID_TO_POOL_NAME"]} PoolIdToPoolName
|
* @typedef {InstanceType<typeof BrkClient>["POOL_ID_TO_POOL_NAME"]} PoolIdToPoolName
|
||||||
* @typedef {keyof PoolIdToPoolName} PoolId
|
* @typedef {keyof PoolIdToPoolName} PoolId
|
||||||
*
|
*
|
||||||
|
* Tree branch types
|
||||||
|
* @typedef {Brk.MetricsTree_Market} Market
|
||||||
|
* @typedef {Brk.MetricsTree_Market_MovingAverage} MarketMovingAverage
|
||||||
|
* @typedef {Brk.MetricsTree_Market_Dca} MarketDca
|
||||||
|
*
|
||||||
* Pattern unions by cohort type
|
* Pattern unions by cohort type
|
||||||
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} UtxoCohortPattern
|
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} UtxoCohortPattern
|
||||||
* @typedef {AddressAmountPattern} AddressCohortPattern
|
* @typedef {AddressAmountPattern} AddressCohortPattern
|
||||||
* @typedef {UtxoCohortPattern | AddressCohortPattern} CohortPattern
|
* @typedef {UtxoCohortPattern | AddressCohortPattern} CohortPattern
|
||||||
*
|
*
|
||||||
* Relative pattern capability types
|
* Relative pattern capability types
|
||||||
* @typedef {RelativePattern | RelativePattern5} RelativeWithMarketCap
|
* @typedef {GlobalRelativePattern | FullRelativePattern} RelativeWithMarketCap
|
||||||
* @typedef {RelativePattern2 | RelativePattern5} RelativeWithOwnMarketCap
|
* @typedef {OwnRelativePattern | FullRelativePattern} RelativeWithOwnMarketCap
|
||||||
* @typedef {RelativePattern2 | RelativePattern5 | AllRelativePattern} RelativeWithOwnPnl
|
* @typedef {OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithOwnPnl
|
||||||
*
|
*
|
||||||
* Capability-based pattern groupings (patterns that have specific properties)
|
* Capability-based pattern groupings (patterns that have specific properties)
|
||||||
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} PatternWithRealizedPrice
|
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} PatternWithRealizedPrice
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
const imports = {
|
|
||||||
async signals() {
|
|
||||||
return import("./signals.js").then((d) => d.default);
|
|
||||||
},
|
|
||||||
async leanQr() {
|
|
||||||
return import("./modules/lean-qr/2.6.1/index.mjs").then((d) => d);
|
|
||||||
},
|
|
||||||
async ufuzzy() {
|
|
||||||
return import("./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs").then(
|
|
||||||
({ default: d }) => d,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
async brkClient() {
|
|
||||||
return import("./modules/brk-client/index.js").then((d) => d);
|
|
||||||
},
|
|
||||||
async resources() {
|
|
||||||
return import("./resources.js").then((d) => d);
|
|
||||||
},
|
|
||||||
|
|
||||||
async chart() {
|
|
||||||
return window.document.fonts.ready.then(() =>
|
|
||||||
import("./chart/index.js").then((d) => d.default),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
async options() {
|
|
||||||
return import("./options/full.js").then((d) => d);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template {keyof typeof imports} K
|
|
||||||
* @param {K} key
|
|
||||||
*/
|
|
||||||
function lazyImport(key) {
|
|
||||||
/** @type {any | null} */
|
|
||||||
let packagePromise = null;
|
|
||||||
|
|
||||||
return function () {
|
|
||||||
if (!packagePromise) {
|
|
||||||
packagePromise = imports[key]();
|
|
||||||
}
|
|
||||||
return /** @type {ReturnType<typeof imports[K]>} */ (packagePromise);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default /** @type {{ [K in keyof typeof imports]: () => ReturnType<typeof imports[K]> }} */ (
|
|
||||||
Object.fromEntries(
|
|
||||||
Object.keys(imports).map((key) => [
|
|
||||||
key,
|
|
||||||
lazyImport(/** @type {keyof typeof imports} */ (key)),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
+498
-554
File diff suppressed because it is too large
Load Diff
@@ -178,7 +178,7 @@ export function fromBitcoin(colors, pattern, title, color) {
|
|||||||
/**
|
/**
|
||||||
* Create series from a SizePattern ({ sum, cumulative, average, min, max, percentiles })
|
* Create series from a SizePattern ({ sum, cumulative, average, min, max, percentiles })
|
||||||
* @param {Colors} colors
|
* @param {Colors} colors
|
||||||
* @param {SizePattern} pattern
|
* @param {AnyStatsPattern} pattern
|
||||||
* @param {string} title
|
* @param {string} title
|
||||||
* @param {Color} [color]
|
* @param {Color} [color]
|
||||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||||
@@ -241,7 +241,7 @@ export function fromBlockSize(colors, pattern, title, color) {
|
|||||||
/**
|
/**
|
||||||
* Create series from a SizePattern ({ average, sum, cumulative, min, max, percentiles })
|
* Create series from a SizePattern ({ average, sum, cumulative, min, max, percentiles })
|
||||||
* @param {Colors} colors
|
* @param {Colors} colors
|
||||||
* @param {SizePattern} pattern
|
* @param {AnyStatsPattern} pattern
|
||||||
* @param {string} title
|
* @param {string} title
|
||||||
* @param {Unit} unit
|
* @param {Unit} unit
|
||||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||||
|
|||||||
@@ -238,8 +238,8 @@
|
|||||||
* @property {HistogramSeriesFn} histogram
|
* @property {HistogramSeriesFn} histogram
|
||||||
* @property {(pattern: BlockCountPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockCount
|
* @property {(pattern: BlockCountPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockCount
|
||||||
* @property {(pattern: FullnessPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBitcoin
|
* @property {(pattern: FullnessPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBitcoin
|
||||||
* @property {(pattern: SizePattern, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockSize
|
* @property {(pattern: AnyStatsPattern, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockSize
|
||||||
* @property {(pattern: SizePattern, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromSizePattern
|
* @property {(pattern: AnyStatsPattern, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromSizePattern
|
||||||
* @property {(pattern: FullnessPattern<any>, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromFullnessPattern
|
* @property {(pattern: FullnessPattern<any>, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromFullnessPattern
|
||||||
* @property {(pattern: FeeRatePattern<any>, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromFeeRatePattern
|
* @property {(pattern: FeeRatePattern<any>, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromFeeRatePattern
|
||||||
* @property {(pattern: CoinbasePattern, title: string) => AnyFetchedSeriesBlueprint[]} fromCoinbasePattern
|
* @property {(pattern: CoinbasePattern, title: string) => AnyFetchedSeriesBlueprint[]} fromCoinbasePattern
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import { ios, canShare } from "../../utils/env.js";
|
|||||||
import { serdeChartableIndex, serdeOptNumber } from "../../utils/serde.js";
|
import { serdeChartableIndex, serdeOptNumber } from "../../utils/serde.js";
|
||||||
import { throttle } from "../../utils/timing.js";
|
import { throttle } from "../../utils/timing.js";
|
||||||
import { Unit } from "../../utils/units.js";
|
import { Unit } from "../../utils/units.js";
|
||||||
|
import signals from "../../signals.js";
|
||||||
|
import { createChartElement } from "../../chart/index.js";
|
||||||
|
import { webSockets } from "../../utils/ws.js";
|
||||||
|
|
||||||
const keyPrefix = "chart";
|
const keyPrefix = "chart";
|
||||||
const ONE_BTC_IN_SATS = 100_000_000;
|
const ONE_BTC_IN_SATS = 100_000_000;
|
||||||
@@ -22,20 +25,12 @@ const CANDLE = "candle";
|
|||||||
/**
|
/**
|
||||||
* @param {Object} args
|
* @param {Object} args
|
||||||
* @param {Colors} args.colors
|
* @param {Colors} args.colors
|
||||||
* @param {CreateChartElement} args.createChartElement
|
|
||||||
* @param {Accessor<ChartOption>} args.option
|
* @param {Accessor<ChartOption>} args.option
|
||||||
* @param {Signals} args.signals
|
|
||||||
* @param {WebSockets} args.webSockets
|
|
||||||
* @param {Resources} args.resources
|
|
||||||
* @param {BrkClient} args.brk
|
* @param {BrkClient} args.brk
|
||||||
*/
|
*/
|
||||||
export function init({
|
export function init({
|
||||||
colors,
|
colors,
|
||||||
createChartElement,
|
|
||||||
option,
|
option,
|
||||||
signals,
|
|
||||||
webSockets,
|
|
||||||
resources,
|
|
||||||
brk,
|
brk,
|
||||||
}) {
|
}) {
|
||||||
chartElement.append(createShadow("left"));
|
chartElement.append(createShadow("left"));
|
||||||
@@ -44,10 +39,7 @@ export function init({
|
|||||||
const { headerElement, headingElement } = createHeader();
|
const { headerElement, headingElement } = createHeader();
|
||||||
chartElement.append(headerElement);
|
chartElement.append(headerElement);
|
||||||
|
|
||||||
const { index, fieldset } = createIndexSelector({
|
const { index, fieldset } = createIndexSelector(option);
|
||||||
option,
|
|
||||||
signals,
|
|
||||||
});
|
|
||||||
|
|
||||||
const TIMERANGE_LS_KEY = signals.createMemo(
|
const TIMERANGE_LS_KEY = signals.createMemo(
|
||||||
() => `chart-timerange-${index()}`,
|
() => `chart-timerange-${index()}`,
|
||||||
@@ -77,7 +69,6 @@ export function init({
|
|||||||
signals,
|
signals,
|
||||||
colors,
|
colors,
|
||||||
id: "charts",
|
id: "charts",
|
||||||
resources,
|
|
||||||
brk,
|
brk,
|
||||||
index,
|
index,
|
||||||
timeScaleSetCallback: (unknownTimeScaleCallback) => {
|
timeScaleSetCallback: (unknownTimeScaleCallback) => {
|
||||||
@@ -525,11 +516,9 @@ export function init({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} args
|
* @param {Accessor<ChartOption>} option
|
||||||
* @param {Accessor<ChartOption>} args.option
|
|
||||||
* @param {Signals} args.signals
|
|
||||||
*/
|
*/
|
||||||
function createIndexSelector({ option, signals }) {
|
function createIndexSelector(option) {
|
||||||
const choices_ = /** @satisfies {ChartableIndexName[]} */ ([
|
const choices_ = /** @satisfies {ChartableIndexName[]} */ ([
|
||||||
"timestamp",
|
"timestamp",
|
||||||
"date",
|
"date",
|
||||||
|
|||||||
@@ -1,25 +1,7 @@
|
|||||||
import { randomFromArray } from "../utils/array.js";
|
import { randomFromArray } from "../utils/array.js";
|
||||||
import { explorerElement } from "../utils/elements.js";
|
import { explorerElement } from "../utils/elements.js";
|
||||||
|
|
||||||
/**
|
export function init() {
|
||||||
* @param {Object} args
|
|
||||||
* @param {Colors} args.colors
|
|
||||||
* @param {CreateChartElement} args.createChartElement
|
|
||||||
* @param {Accessor<ChartOption>} args.option
|
|
||||||
* @param {Signals} args.signals
|
|
||||||
* @param {WebSockets} args.webSockets
|
|
||||||
* @param {Resources} args.resources
|
|
||||||
* @param {BrkClient} args.brk
|
|
||||||
*/
|
|
||||||
export function init({
|
|
||||||
colors: _colors,
|
|
||||||
createChartElement: _createChartElement,
|
|
||||||
option: _option,
|
|
||||||
signals: _signals,
|
|
||||||
webSockets: _webSockets,
|
|
||||||
resources: _resources,
|
|
||||||
brk: _brk,
|
|
||||||
}) {
|
|
||||||
const chain = window.document.createElement("div");
|
const chain = window.document.createElement("div");
|
||||||
chain.id = "chain";
|
chain.id = "chain";
|
||||||
explorerElement.append(chain);
|
explorerElement.append(chain);
|
||||||
|
|||||||
@@ -18,15 +18,15 @@ import {
|
|||||||
numberToUSNumber,
|
numberToUSNumber,
|
||||||
} from "../utils/format.js";
|
} from "../utils/format.js";
|
||||||
import { serdeDate, serdeOptDate, serdeOptNumber } from "../utils/serde.js";
|
import { serdeDate, serdeOptDate, serdeOptNumber } from "../utils/serde.js";
|
||||||
|
import signals from "../signals.js";
|
||||||
|
import { createChartElement } from "../chart/index.js";
|
||||||
|
import { resources } from "../resources.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} args
|
* @param {Object} args
|
||||||
* @param {Colors} args.colors
|
* @param {Colors} args.colors
|
||||||
* @param {CreateChartElement} args.createChartElement
|
|
||||||
* @param {Signals} args.signals
|
|
||||||
* @param {Resources} args.resources
|
|
||||||
*/
|
*/
|
||||||
export function init({ colors, createChartElement, signals, resources }) {
|
export function init({ colors }) {
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Frequency
|
* @typedef {Object} Frequency
|
||||||
* @property {string} name
|
* @property {string} name
|
||||||
|
|||||||
@@ -6,14 +6,7 @@ import { tableElement } from "../utils/elements.js";
|
|||||||
import { serdeMetrics, serdeString } from "../utils/serde.js";
|
import { serdeMetrics, serdeString } from "../utils/serde.js";
|
||||||
import { resetParams } from "../utils/url.js";
|
import { resetParams } from "../utils/url.js";
|
||||||
|
|
||||||
/**
|
export function init() {
|
||||||
* @param {Object} args
|
|
||||||
* @param {Signals} args.signals
|
|
||||||
* @param {Option} args.option
|
|
||||||
* @param {Resources} args.resources
|
|
||||||
* @param {BrkClient} args.brk
|
|
||||||
*/
|
|
||||||
export function init({ signals, option, resources, brk }) {
|
|
||||||
tableElement.innerHTML = "wip, will hopefuly be back soon, sorry !";
|
tableElement.innerHTML = "wip, will hopefuly be back soon, sorry !";
|
||||||
|
|
||||||
// const parent = tableElement;
|
// const parent = tableElement;
|
||||||
|
|||||||
@@ -25,105 +25,106 @@
|
|||||||
/** @typedef {MetricResource<unknown>} AnyMetricResource */
|
/** @typedef {MetricResource<unknown>} AnyMetricResource */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {ReturnType<typeof createResources>} Resources
|
* @typedef {{ createResource: typeof createResource, useMetricEndpoint: typeof useMetricEndpoint }} Resources
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import signals from "./signals.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Signals} signals
|
* Create a generic reactive resource wrapper for any async fetcher
|
||||||
|
* @template T
|
||||||
|
* @template {any[]} Args
|
||||||
|
* @param {(...args: Args) => Promise<T>} fetcher
|
||||||
|
* @returns {Resource<T>}
|
||||||
*/
|
*/
|
||||||
export function createResources(signals) {
|
function createResource(fetcher) {
|
||||||
const owner = signals.getOwner();
|
const owner = signals.getOwner();
|
||||||
|
return signals.runWithOwner(owner, () => {
|
||||||
|
const data = signals.createSignal(/** @type {T | null} */ (null));
|
||||||
|
const loading = signals.createSignal(false);
|
||||||
|
const error = signals.createSignal(/** @type {Error | null} */ (null));
|
||||||
|
|
||||||
/**
|
return {
|
||||||
* Create a generic reactive resource wrapper for any async fetcher
|
data,
|
||||||
* @template T
|
loading,
|
||||||
* @template {any[]} Args
|
error,
|
||||||
* @param {(...args: Args) => Promise<T>} fetcher
|
|
||||||
* @returns {Resource<T>}
|
|
||||||
*/
|
|
||||||
function createResource(fetcher) {
|
|
||||||
return signals.runWithOwner(owner, () => {
|
|
||||||
const data = signals.createSignal(/** @type {T | null} */ (null));
|
|
||||||
const loading = signals.createSignal(false);
|
|
||||||
const error = signals.createSignal(/** @type {Error | null} */ (null));
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
/**
|
|
||||||
* @param {Args} args
|
|
||||||
*/
|
|
||||||
async fetch(...args) {
|
|
||||||
loading.set(true);
|
|
||||||
error.set(null);
|
|
||||||
try {
|
|
||||||
const result = await fetcher(...args);
|
|
||||||
data.set(() => result);
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
error.set(e instanceof Error ? e : new Error(String(e)));
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
loading.set(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a reactive resource wrapper for a MetricEndpoint with multi-range support
|
|
||||||
* @template T
|
|
||||||
* @param {MetricEndpoint<T>} endpoint
|
|
||||||
* @returns {MetricResource<T>}
|
|
||||||
*/
|
|
||||||
function useMetricEndpoint(endpoint) {
|
|
||||||
return signals.runWithOwner(owner, () => {
|
|
||||||
/** @type {Map<string, RangeState<T>>} */
|
|
||||||
const ranges = new Map();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or create range state
|
* @param {Args} args
|
||||||
* @param {number} [from=-10000]
|
|
||||||
* @param {number} [to]
|
|
||||||
* @returns {RangeState<T>}
|
|
||||||
*/
|
*/
|
||||||
function range(from = -10000, to) {
|
async fetch(...args) {
|
||||||
const key = `${from}-${to ?? ""}`;
|
loading.set(true);
|
||||||
const existing = ranges.get(key);
|
error.set(null);
|
||||||
if (existing) return existing;
|
try {
|
||||||
|
const result = await fetcher(...args);
|
||||||
/** @type {RangeState<T>} */
|
data.set(() => result);
|
||||||
const state = {
|
return result;
|
||||||
response: signals.createSignal(/** @type {MetricData<T> | null} */ (null)),
|
} catch (e) {
|
||||||
loading: signals.createSignal(false),
|
error.set(e instanceof Error ? e : new Error(String(e)));
|
||||||
};
|
return null;
|
||||||
ranges.set(key, state);
|
} finally {
|
||||||
return state;
|
loading.set(false);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
return {
|
};
|
||||||
path: endpoint.path,
|
});
|
||||||
range,
|
|
||||||
/**
|
|
||||||
* Fetch data for a range
|
|
||||||
* @param {number} [from=-10000]
|
|
||||||
* @param {number} [to]
|
|
||||||
*/
|
|
||||||
async fetch(from = -10000, to) {
|
|
||||||
const r = range(from, to);
|
|
||||||
r.loading.set(true);
|
|
||||||
try {
|
|
||||||
const result = await endpoint.range(from, to, r.response.set);
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
r.loading.set(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { createResource, useMetricEndpoint };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a reactive resource wrapper for a MetricEndpoint with multi-range support
|
||||||
|
* @template T
|
||||||
|
* @param {MetricEndpoint<T>} endpoint
|
||||||
|
* @returns {MetricResource<T>}
|
||||||
|
*/
|
||||||
|
function useMetricEndpoint(endpoint) {
|
||||||
|
const owner = signals.getOwner();
|
||||||
|
return signals.runWithOwner(owner, () => {
|
||||||
|
/** @type {Map<string, RangeState<T>>} */
|
||||||
|
const ranges = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create range state
|
||||||
|
* @param {number} [from=-10000]
|
||||||
|
* @param {number} [to]
|
||||||
|
* @returns {RangeState<T>}
|
||||||
|
*/
|
||||||
|
function range(from = -10000, to) {
|
||||||
|
const key = `${from}-${to ?? ""}`;
|
||||||
|
const existing = ranges.get(key);
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
|
/** @type {RangeState<T>} */
|
||||||
|
const state = {
|
||||||
|
response: signals.createSignal(
|
||||||
|
/** @type {MetricData<T> | null} */ (null),
|
||||||
|
),
|
||||||
|
loading: signals.createSignal(false),
|
||||||
|
};
|
||||||
|
ranges.set(key, state);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: endpoint.path,
|
||||||
|
range,
|
||||||
|
/**
|
||||||
|
* Fetch data for a range
|
||||||
|
* @param {number} [start=-10000]
|
||||||
|
* @param {number} [end]
|
||||||
|
*/
|
||||||
|
async fetch(start = -10000, end) {
|
||||||
|
const r = range(start, end);
|
||||||
|
r.loading.set(true);
|
||||||
|
try {
|
||||||
|
const result = await endpoint
|
||||||
|
.slice(start, end)
|
||||||
|
.fetch(r.response.set);
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
r.loading.set(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resources = { createResource, useMetricEndpoint };
|
||||||
|
|||||||
@@ -1,128 +1,114 @@
|
|||||||
|
import signals from "../signals.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Signals} signals
|
* @template T
|
||||||
|
* @param {(callback: (value: T) => void) => WebSocket} creator
|
||||||
*/
|
*/
|
||||||
export function createWebSockets(signals) {
|
function createWebsocket(creator) {
|
||||||
/**
|
let ws = /** @type {WebSocket | null} */ (null);
|
||||||
* @template T
|
|
||||||
* @param {(callback: (value: T) => void) => WebSocket} creator
|
|
||||||
*/
|
|
||||||
function createWebsocket(creator) {
|
|
||||||
let ws = /** @type {WebSocket | null} */ (null);
|
|
||||||
|
|
||||||
const live = signals.createSignal(false);
|
const live = signals.createSignal(false);
|
||||||
const latest = signals.createSignal(/** @type {T | null} */ (null));
|
const latest = signals.createSignal(/** @type {T | null} */ (null));
|
||||||
|
|
||||||
function reinitWebSocket() {
|
function reinitWebSocket() {
|
||||||
if (!ws || ws.readyState === ws.CLOSED) {
|
if (!ws || ws.readyState === ws.CLOSED) {
|
||||||
console.log("ws: reinit");
|
console.log("ws: reinit");
|
||||||
resource.open();
|
resource.open();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function reinitWebSocketIfDocumentNotHidden() {
|
function reinitWebSocketIfDocumentNotHidden() {
|
||||||
!window.document.hidden && reinitWebSocket();
|
!window.document.hidden && reinitWebSocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
const resource = {
|
const resource = {
|
||||||
live,
|
live,
|
||||||
latest,
|
latest,
|
||||||
open() {
|
open() {
|
||||||
ws = creator((value) => latest.set(() => value));
|
ws = creator((value) => latest.set(() => value));
|
||||||
|
|
||||||
ws.addEventListener("open", () => {
|
ws.addEventListener("open", () => {
|
||||||
console.log("ws: open");
|
console.log("ws: open");
|
||||||
live.set(true);
|
live.set(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener("close", () => {
|
ws.addEventListener("close", () => {
|
||||||
console.log("ws: close");
|
console.log("ws: close");
|
||||||
live.set(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.document.addEventListener(
|
|
||||||
"visibilitychange",
|
|
||||||
reinitWebSocketIfDocumentNotHidden,
|
|
||||||
);
|
|
||||||
|
|
||||||
window.document.addEventListener("online", reinitWebSocket);
|
|
||||||
},
|
|
||||||
close() {
|
|
||||||
ws?.close();
|
|
||||||
window.document.removeEventListener(
|
|
||||||
"visibilitychange",
|
|
||||||
reinitWebSocketIfDocumentNotHidden,
|
|
||||||
);
|
|
||||||
window.document.removeEventListener("online", reinitWebSocket);
|
|
||||||
live.set(false);
|
live.set(false);
|
||||||
ws = null;
|
});
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return resource;
|
window.document.addEventListener(
|
||||||
}
|
"visibilitychange",
|
||||||
|
reinitWebSocketIfDocumentNotHidden,
|
||||||
/**
|
|
||||||
* @param {(candle: CandlestickData) => void} callback
|
|
||||||
*/
|
|
||||||
function krakenCandleWebSocketCreator(callback) {
|
|
||||||
const ws = new WebSocket("wss://ws.kraken.com/v2");
|
|
||||||
|
|
||||||
ws.addEventListener("open", () => {
|
|
||||||
ws.send(
|
|
||||||
JSON.stringify({
|
|
||||||
method: "subscribe",
|
|
||||||
params: {
|
|
||||||
channel: "ohlc",
|
|
||||||
symbol: ["BTC/USD"],
|
|
||||||
interval: 1440,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
ws.addEventListener("message", (message) => {
|
window.document.addEventListener("online", reinitWebSocket);
|
||||||
const result = JSON.parse(message.data);
|
},
|
||||||
|
close() {
|
||||||
|
ws?.close();
|
||||||
|
window.document.removeEventListener(
|
||||||
|
"visibilitychange",
|
||||||
|
reinitWebSocketIfDocumentNotHidden,
|
||||||
|
);
|
||||||
|
window.document.removeEventListener("online", reinitWebSocket);
|
||||||
|
live.set(false);
|
||||||
|
ws = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if (result.channel !== "ohlc") return;
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
const { interval_begin, open, high, low, close } = result.data.at(-1);
|
/**
|
||||||
|
* @param {(candle: CandlestickData) => void} callback
|
||||||
|
*/
|
||||||
|
function krakenCandleWebSocketCreator(callback) {
|
||||||
|
const ws = new WebSocket("wss://ws.kraken.com/v2");
|
||||||
|
|
||||||
/** @type {CandlestickData} */
|
ws.addEventListener("open", () => {
|
||||||
const candle = {
|
ws.send(
|
||||||
// index: -1,
|
JSON.stringify({
|
||||||
time: new Date(interval_begin).valueOf() / 1000,
|
method: "subscribe",
|
||||||
open: Number(open),
|
params: {
|
||||||
high: Number(high),
|
channel: "ohlc",
|
||||||
low: Number(low),
|
symbol: ["BTC/USD"],
|
||||||
close: Number(close),
|
interval: 1440,
|
||||||
};
|
},
|
||||||
|
}),
|
||||||
candle && callback({ ...candle });
|
);
|
||||||
});
|
|
||||||
|
|
||||||
return ws;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {ReturnType<typeof createWebsocket<CandlestickData>>} */
|
|
||||||
const kraken1dCandle = createWebsocket((callback) =>
|
|
||||||
krakenCandleWebSocketCreator(callback),
|
|
||||||
);
|
|
||||||
|
|
||||||
kraken1dCandle.open();
|
|
||||||
|
|
||||||
signals.createEffect(kraken1dCandle.latest, (latest) => {
|
|
||||||
if (latest) {
|
|
||||||
const close = latest.close;
|
|
||||||
console.log("close:", close);
|
|
||||||
|
|
||||||
window.document.title = `${latest.close.toLocaleString("en-us")} | ${
|
|
||||||
window.location.host
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
ws.addEventListener("message", (message) => {
|
||||||
kraken1dCandle,
|
const result = JSON.parse(message.data);
|
||||||
};
|
|
||||||
|
if (result.channel !== "ohlc") return;
|
||||||
|
|
||||||
|
const { interval_begin, open, high, low, close } = result.data.at(-1);
|
||||||
|
|
||||||
|
/** @type {CandlestickData} */
|
||||||
|
const candle = {
|
||||||
|
// index: -1,
|
||||||
|
time: new Date(interval_begin).valueOf() / 1000,
|
||||||
|
open: Number(open),
|
||||||
|
high: Number(high),
|
||||||
|
low: Number(low),
|
||||||
|
close: Number(close),
|
||||||
|
};
|
||||||
|
|
||||||
|
candle && callback({ ...candle });
|
||||||
|
});
|
||||||
|
|
||||||
|
return ws;
|
||||||
}
|
}
|
||||||
/** @typedef {ReturnType<typeof createWebSockets>} WebSockets */
|
|
||||||
|
/** @type {ReturnType<typeof createWebsocket<CandlestickData>>} */
|
||||||
|
const kraken1dCandle = createWebsocket((callback) =>
|
||||||
|
krakenCandleWebSocketCreator(callback),
|
||||||
|
);
|
||||||
|
|
||||||
|
kraken1dCandle.open();
|
||||||
|
|
||||||
|
export const webSockets = {
|
||||||
|
kraken1dCandle,
|
||||||
|
};
|
||||||
|
/** @typedef {typeof webSockets} WebSockets */
|
||||||
|
|||||||
Reference in New Issue
Block a user