mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-30 07:29:27 -07:00
global: snapshot
This commit is contained in:
24
Cargo.lock
generated
24
Cargo.lock
generated
@@ -1954,9 +1954,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "importmap"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d13d6361899f14d58146b6214c07e63cda8270c3ef3b8c30626f8e20e6766eb"
|
||||
checksum = "a45c4c5e8b4d0a6aed90f3a14125bd3af3bca73d49d44e8f8b764311903b5213"
|
||||
dependencies = [
|
||||
"rapidhash",
|
||||
"serde",
|
||||
@@ -2684,7 +2684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
"rand_core 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2704,7 +2704,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.3",
|
||||
"rand_core 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2718,9 +2718,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||
checksum = "4f1b3bc831f92381018fd9c6350b917c7b21f1eed35a65a51900e0e55a3d7afa"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
@@ -2745,9 +2745,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rawdb"
|
||||
version = "0.5.8"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1eb09ba02f9467845fde4a1fadb317721025f2b836f22a5a7d3567c9e100875"
|
||||
checksum = "eb8999fc8f80521c13091f98e1a24bde2448b29c0637f008dc470ed84d58f703"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
@@ -3689,9 +3689,9 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
|
||||
|
||||
[[package]]
|
||||
name = "vecdb"
|
||||
version = "0.5.8"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b02690e7c013257b959b482fac78e90f73764efa8a57551e1e67a06ad7ab4"
|
||||
checksum = "1a31985bee527adda1601eab9c72de0a567be83a08e80e646b79afb77ab14970"
|
||||
dependencies = [
|
||||
"ctrlc",
|
||||
"log",
|
||||
@@ -3710,9 +3710,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vecdb_derive"
|
||||
version = "0.5.8"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21339c58345d1a422c2574b1114b3cd862900a9196421dc7acf43aca48c288bb"
|
||||
checksum = "61716cbeb322dcc9838cea11b533cea278f9592b2ef2ce520595849833cfee1c"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
|
||||
@@ -79,7 +79,7 @@ serde_json = { version = "1.0.149", features = ["float_roundtrip", "preserve_ord
|
||||
smallvec = "1.15.1"
|
||||
tokio = { version = "1.49.0", features = ["rt-multi-thread"] }
|
||||
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"] }
|
||||
|
||||
[workspace.metadata.release]
|
||||
|
||||
@@ -458,4 +458,33 @@ mod tests {
|
||||
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 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.
|
||||
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.
|
||||
///
|
||||
/// Uses field names and first leaf names from direct children to determine
|
||||
/// 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);
|
||||
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.
|
||||
@@ -217,3 +282,93 @@ pub fn get_fields_with_child_info(
|
||||
})
|
||||
.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 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`.
|
||||
pub struct TreeNodeContext<'a> {
|
||||
/// The children of the branch node.
|
||||
pub children: &'a std::collections::BTreeMap<String, TreeNode>,
|
||||
/// 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>,
|
||||
/// Pre-computed context for each child.
|
||||
pub children: Vec<ChildContext<'a>>,
|
||||
}
|
||||
|
||||
/// Prepare a tree node for generation.
|
||||
@@ -26,20 +45,22 @@ pub fn prepare_tree_node<'a>(
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
) -> Option<TreeNodeContext<'a>> {
|
||||
let TreeNode::Branch(children) = node else {
|
||||
let TreeNode::Branch(branch_children) = node else {
|
||||
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
|
||||
.iter()
|
||||
.map(|(f, _)| f.clone())
|
||||
.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)
|
||||
&& pattern_name != name
|
||||
&& metadata.is_parameterizable(pattern_name)
|
||||
&& !base_result.has_outlier
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -50,9 +71,38 @@ pub fn prepare_tree_node<'a>(
|
||||
}
|
||||
generated.insert(name.to_string());
|
||||
|
||||
Some(TreeNodeContext {
|
||||
children,
|
||||
fields_with_child_info,
|
||||
fields,
|
||||
})
|
||||
// Build child contexts with pre-computed decisions
|
||||
let children: Vec<ChildContext<'a>> = branch_children
|
||||
.iter()
|
||||
.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 {{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
|
||||
* @typedef {{Object}} MetricEndpointBuilder
|
||||
* @property {{(n: number) => RangeBuilder<T>}} first - Fetch first n data points
|
||||
* @property {{(n: number) => RangeBuilder<T>}} last - Fetch last n data points
|
||||
* @property {{(start: number, end: number) => RangeBuilder<T>}} range - Set explicit range [start, end)
|
||||
* @property {{(start: number) => FromBuilder<T>}} from - Set start position, chain with take() or to()
|
||||
* @property {{(end: number) => ToBuilder<T>}} to - Set end position, chain with takeLast() or from()
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON (all data)
|
||||
* @property {{() => Promise<string>}} csv - Execute and return CSV (all data)
|
||||
* @property {{(index: number) => SingleItemBuilder<T>}} get - Get single item at index
|
||||
* @property {{(start?: number, end?: number) => RangeBuilder<T>}} slice - Slice like Array.slice
|
||||
* @property {{(n: number) => RangeBuilder<T>}} first - Get first n items
|
||||
* @property {{(n: number) => RangeBuilder<T>}} last - Get last n items
|
||||
* @property {{(n: number) => SkippedBuilder<T>}} skip - Skip first n items, chain with take()
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch all data
|
||||
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
|
||||
* @property {{Thenable<T>}} then - Thenable (await endpoint)
|
||||
* @property {{string}} path - The endpoint path
|
||||
*/
|
||||
/** @typedef {{MetricEndpointBuilder<unknown>}} AnyMetricEndpointBuilder */
|
||||
/** @typedef {{MetricEndpointBuilder<any>}} AnyMetricEndpointBuilder */
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{Object}} FromBuilder
|
||||
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items from start position
|
||||
* @property {{(end: number) => RangeBuilder<T>}} to - Set end position
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON
|
||||
* @property {{() => Promise<string>}} csv - Execute and return CSV
|
||||
* @typedef {{Object}} SingleItemBuilder
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the item
|
||||
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
|
||||
* @property {{Thenable<T>}} then - Thenable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{Object}} ToBuilder
|
||||
* @property {{(n: number) => RangeBuilder<T>}} takeLast - Take last n items before end position
|
||||
* @property {{(start: number) => RangeBuilder<T>}} from - Set start position
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON
|
||||
* @property {{() => Promise<string>}} csv - Execute and return CSV
|
||||
* @typedef {{Object}} SkippedBuilder
|
||||
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items after skipped position
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch from skipped position to end
|
||||
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
|
||||
* @property {{Thenable<T>}} then - Thenable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{Object}} RangeBuilder
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON
|
||||
* @property {{() => Promise<string>}} csv - Execute and return CSV
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the range
|
||||
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
|
||||
* @property {{Thenable<T>}} then - Thenable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{Object}} MetricPattern
|
||||
* @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: 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.
|
||||
@@ -147,50 +155,46 @@ function _endpoint(client, name, index) {{
|
||||
* @returns {{RangeBuilder<T>}}
|
||||
*/
|
||||
const rangeBuilder = (start, end) => ({{
|
||||
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
|
||||
return client.getJson(buildPath(start, end), onUpdate);
|
||||
}},
|
||||
csv() {{ return client.getText(buildPath(start, end, 'csv')); }},
|
||||
fetch(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); }},
|
||||
}});
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @returns {{FromBuilder<T>}}
|
||||
* @returns {{SkippedBuilder<T>}}
|
||||
*/
|
||||
const fromBuilder = (start) => ({{
|
||||
take(/** @type {{number}} */ n) {{ return rangeBuilder(start, start + n); }},
|
||||
to(/** @type {{number}} */ end) {{ return rangeBuilder(start, end); }},
|
||||
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
|
||||
return client.getJson(buildPath(start, undefined), onUpdate);
|
||||
}},
|
||||
csv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
|
||||
const skippedBuilder = (start) => ({{
|
||||
take(n) {{ return rangeBuilder(start, start + n); }},
|
||||
fetch(onUpdate) {{ return client.getJson(buildPath(start, undefined), onUpdate); }},
|
||||
fetchCsv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
|
||||
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||
}});
|
||||
|
||||
/**
|
||||
* @param {{number}} end
|
||||
* @returns {{ToBuilder<T>}}
|
||||
*/
|
||||
const toBuilder = (end) => ({{
|
||||
takeLast(/** @type {{number}} */ n) {{ return rangeBuilder(end - n, end); }},
|
||||
from(/** @type {{number}} */ start) {{ return rangeBuilder(start, end); }},
|
||||
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
|
||||
return client.getJson(buildPath(undefined, end), onUpdate);
|
||||
}},
|
||||
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')); }},
|
||||
/** @type {{MetricEndpointBuilder<T>}} */
|
||||
const endpoint = {{
|
||||
get(index) {{ return singleItemBuilder(index); }},
|
||||
slice(start, end) {{ return rangeBuilder(start, end); }},
|
||||
first(n) {{ return rangeBuilder(undefined, n); }},
|
||||
last(n) {{ return rangeBuilder(-n, undefined); }},
|
||||
skip(n) {{ return skippedBuilder(n); }},
|
||||
fetch(onUpdate) {{ return client.getJson(buildPath(), onUpdate); }},
|
||||
fetchCsv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
|
||||
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||
get path() {{ return p; }},
|
||||
}};
|
||||
|
||||
return endpoint;
|
||||
}}
|
||||
|
||||
/**
|
||||
@@ -235,7 +239,7 @@ class BrkClientBase {{
|
||||
const cachedJson = cachedRes ? await cachedRes.json() : null;
|
||||
|
||||
if (cachedJson) onUpdate?.(cachedJson);
|
||||
if (!globalThis.navigator?.onLine) {{
|
||||
if (globalThis.navigator?.onLine === false) {{
|
||||
if (cachedJson) return cachedJson;
|
||||
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) {
|
||||
let json_value: Value = serde_json::to_value(value).unwrap();
|
||||
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);
|
||||
@@ -336,13 +344,25 @@ fn camel_case_top_level_keys(value: Value) -> Value {
|
||||
fn indent_json_const(json: &str) -> String {
|
||||
json.lines()
|
||||
.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<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -354,14 +374,30 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
|
||||
writeln!(output, "// Index accessor factory functions\n").unwrap();
|
||||
|
||||
for pattern in patterns {
|
||||
// Use 'readonly' to indicate these are getters (lazy evaluation)
|
||||
let by_fields: Vec<String> = pattern
|
||||
.indexes
|
||||
.iter()
|
||||
.map(|idx| format!("{}: MetricEndpointBuilder<T>", idx.serialize_long()))
|
||||
.map(|idx| {
|
||||
format!(
|
||||
"readonly {}: MetricEndpointBuilder<T>",
|
||||
idx.serialize_long()
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let by_type = format!("{{ {} }}", by_fields.join(", "));
|
||||
|
||||
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,
|
||||
@@ -385,7 +421,11 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
|
||||
|
||||
for (i, index) in pattern.indexes.iter().enumerate() {
|
||||
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!(
|
||||
output,
|
||||
" get {}() {{ return _endpoint(client, name, '{}'); }}{}",
|
||||
@@ -438,8 +478,12 @@ pub fn generate_structural_patterns(
|
||||
}
|
||||
writeln!(output, " * @typedef {{Object}} {}", pattern.name).unwrap();
|
||||
for field in &pattern.fields {
|
||||
let js_type =
|
||||
metadata.field_type_annotation(field, pattern.is_generic, None, GenericSyntax::JAVASCRIPT);
|
||||
let js_type = metadata.field_type_annotation(
|
||||
field,
|
||||
pattern.is_generic,
|
||||
None,
|
||||
GenericSyntax::JAVASCRIPT,
|
||||
);
|
||||
writeln!(
|
||||
output,
|
||||
" * @property {{{}}} {}",
|
||||
@@ -469,8 +513,17 @@ pub fn generate_structural_patterns(
|
||||
writeln!(output, " * @returns {{{}}}", return_type).unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
|
||||
let param_name = if is_parameterizable { "acc" } else { "basePath" };
|
||||
writeln!(output, "function create{}(client, {}) {{", pattern.name, param_name).unwrap();
|
||||
let param_name = if is_parameterizable {
|
||||
"acc"
|
||||
} else {
|
||||
"basePath"
|
||||
};
|
||||
writeln!(
|
||||
output,
|
||||
"function create{}(client, {}) {{",
|
||||
pattern.name, param_name
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " return {{").unwrap();
|
||||
|
||||
let syntax = JavaScriptSyntax;
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::fmt::Write;
|
||||
use brk_types::TreeNode;
|
||||
|
||||
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,
|
||||
infer_accumulated_name, prepare_tree_node, to_camel_case,
|
||||
};
|
||||
@@ -45,43 +45,41 @@ fn generate_tree_typedef(
|
||||
writeln!(output, "/**").unwrap();
|
||||
writeln!(output, " * @typedef {{Object}} {}", name).unwrap();
|
||||
|
||||
for ((field, child_fields), (child_name, _)) in
|
||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
||||
{
|
||||
let js_type = metadata.resolve_tree_field_type(
|
||||
field,
|
||||
child_fields.as_deref(),
|
||||
name,
|
||||
child_name,
|
||||
GenericSyntax::JAVASCRIPT,
|
||||
);
|
||||
for child in &ctx.children {
|
||||
let js_type = if child.should_inline {
|
||||
child.inline_type_name.clone()
|
||||
} else {
|
||||
metadata.resolve_tree_field_type(
|
||||
&child.field,
|
||||
child.child_fields.as_deref(),
|
||||
name,
|
||||
child.name,
|
||||
GenericSyntax::JAVASCRIPT,
|
||||
)
|
||||
};
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
" * @property {{{}}} {}",
|
||||
js_type,
|
||||
to_camel_case(&field.name)
|
||||
to_camel_case(&child.field.name)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, " */\n").unwrap();
|
||||
|
||||
for (child_name, child_node) in ctx.children {
|
||||
if let TreeNode::Branch(grandchildren) = child_node {
|
||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
||||
// Generate typedef if no pattern match OR pattern is not parameterizable
|
||||
if !metadata.is_parameterizable_fields(&child_fields) {
|
||||
let child_type = child_type_name(name, child_name);
|
||||
generate_tree_typedef(
|
||||
output,
|
||||
&child_type,
|
||||
child_node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
// Generate child typedefs
|
||||
for child in &ctx.children {
|
||||
if child.should_inline {
|
||||
generate_tree_typedef(
|
||||
output,
|
||||
&child.inline_type_name,
|
||||
child.node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,12 +180,14 @@ fn generate_tree_initializer(
|
||||
.get(&child_fields)
|
||||
.filter(|name| metadata.is_parameterizable(name));
|
||||
|
||||
if let Some(pattern_name) = pattern_name {
|
||||
let arg = get_pattern_instance_base(child_node);
|
||||
let base_result = 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!(
|
||||
output,
|
||||
"{}{}: create{}(this, '{}'),",
|
||||
indent_str, field_name, pattern_name, arg
|
||||
indent_str, field_name, pattern_name, base_result.base
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
|
||||
@@ -195,143 +195,138 @@ class _EndpointConfig:
|
||||
|
||||
|
||||
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):
|
||||
self._config = config
|
||||
|
||||
def json(self) -> MetricData[T]:
|
||||
"""Execute the query and return parsed JSON data."""
|
||||
def fetch(self) -> MetricData[T]:
|
||||
"""Fetch the range as parsed JSON."""
|
||||
return self._config.get_json()
|
||||
|
||||
def csv(self) -> str:
|
||||
"""Execute the query and return CSV data as a string."""
|
||||
def fetch_csv(self) -> str:
|
||||
"""Fetch the range as CSV string."""
|
||||
return self._config.get_csv()
|
||||
|
||||
|
||||
class FromBuilder(Generic[T]):
|
||||
"""Builder after calling from(start). Can chain with take() or to()."""
|
||||
class SingleItemBuilder(Generic[T]):
|
||||
"""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):
|
||||
self._config = config
|
||||
|
||||
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
|
||||
return RangeBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
start, start + n
|
||||
))
|
||||
|
||||
def to(self, end: int) -> RangeBuilder[T]:
|
||||
"""Set the end position."""
|
||||
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)."""
|
||||
def fetch(self) -> MetricData[T]:
|
||||
"""Fetch from skipped position 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()
|
||||
|
||||
|
||||
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."""
|
||||
def fetch_csv(self) -> str:
|
||||
"""Fetch as CSV."""
|
||||
return self._config.get_csv()
|
||||
|
||||
|
||||
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:
|
||||
# Get all data
|
||||
endpoint.json()
|
||||
# Fetch all data
|
||||
data = endpoint.fetch()
|
||||
|
||||
# Get last 10 points
|
||||
endpoint.last(10).json()
|
||||
# Single item access
|
||||
data = endpoint[5].fetch()
|
||||
|
||||
# Get range [100, 200)
|
||||
endpoint.range(100, 200).json()
|
||||
# Slice syntax (Python-native)
|
||||
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
|
||||
endpoint.from_(100).take(10).json()
|
||||
# Convenience methods (pandas-style)
|
||||
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):
|
||||
self._config = _EndpointConfig(client, name, index)
|
||||
|
||||
def first(self, n: int) -> RangeBuilder[T]:
|
||||
"""Fetch the first n data points."""
|
||||
@overload
|
||||
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(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
None, n
|
||||
))
|
||||
|
||||
def last(self, n: int) -> RangeBuilder[T]:
|
||||
"""Fetch the last n data points."""
|
||||
def tail(self, n: int = 10) -> RangeBuilder[T]:
|
||||
"""Get the last n items (pandas-style)."""
|
||||
return RangeBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
-n, None
|
||||
))
|
||||
|
||||
def range(self, start: int, end: int) -> RangeBuilder[T]:
|
||||
"""Set an explicit range [start, end)."""
|
||||
return RangeBuilder(_EndpointConfig(
|
||||
def skip(self, n: int) -> SkippedBuilder[T]:
|
||||
"""Skip the first n items. Chain with take() to get a range."""
|
||||
return SkippedBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
start, end
|
||||
n, None
|
||||
))
|
||||
|
||||
def from_(self, start: int) -> FromBuilder[T]:
|
||||
"""Set the start position. Chain with take() or to()."""
|
||||
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)."""
|
||||
def fetch(self) -> MetricData[T]:
|
||||
"""Fetch all data as parsed JSON."""
|
||||
return self._config.get_json()
|
||||
|
||||
def csv(self) -> str:
|
||||
"""Execute the query and return CSV data as a string (all data)."""
|
||||
def fetch_csv(self) -> str:
|
||||
"""Fetch all data as CSV string."""
|
||||
return self._config.get_csv()
|
||||
|
||||
def path(self) -> str:
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn generate_python_client(
|
||||
writeln!(output, "# Do not edit manually\n").unwrap();
|
||||
writeln!(
|
||||
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();
|
||||
writeln!(output, "from http.client import HTTPSConnection, HTTPConnection").unwrap();
|
||||
|
||||
@@ -6,8 +6,8 @@ use std::fmt::Write;
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, child_type_name, generate_leaf_field,
|
||||
get_node_fields, get_pattern_instance_base, prepare_tree_node, to_snake_case,
|
||||
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, generate_leaf_field,
|
||||
prepare_tree_node, to_snake_case,
|
||||
};
|
||||
|
||||
/// Generate tree classes
|
||||
@@ -41,22 +41,16 @@ fn generate_tree_class(
|
||||
|
||||
// Generate child classes FIRST (post-order traversal)
|
||||
// This ensures children are defined before parent references them
|
||||
for (child_name, child_node) in ctx.children.iter() {
|
||||
if let TreeNode::Branch(grandchildren) = child_node {
|
||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
||||
|
||||
// Generate inline class if no pattern match OR pattern is not parameterizable
|
||||
if !metadata.is_parameterizable_fields(&child_fields) {
|
||||
let child_class = child_type_name(name, child_name);
|
||||
generate_tree_class(
|
||||
output,
|
||||
&child_class,
|
||||
child_node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
for child in &ctx.children {
|
||||
if child.should_inline {
|
||||
generate_tree_class(
|
||||
output,
|
||||
&child.inline_type_name,
|
||||
child.node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,45 +65,36 @@ fn generate_tree_class(
|
||||
.unwrap();
|
||||
|
||||
let syntax = PythonSyntax;
|
||||
for ((field, child_fields_opt), (child_name, child_node)) in
|
||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
||||
{
|
||||
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);
|
||||
for child in &ctx.children {
|
||||
let field_name_py = to_snake_case(child.name);
|
||||
|
||||
if metadata.is_pattern_type(&field.rust_type) && metadata.is_parameterizable(&field.rust_type)
|
||||
{
|
||||
// Parameterizable pattern: use pattern class with metric base
|
||||
let metric_base = get_pattern_instance_base(child_node);
|
||||
writeln!(
|
||||
output,
|
||||
" 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);
|
||||
if child.is_leaf {
|
||||
if let TreeNode::Leaf(leaf) = child.node {
|
||||
generate_leaf_field(output, &syntax, "client", child.name, leaf, metadata, " ");
|
||||
}
|
||||
} else if child.should_inline {
|
||||
// Inline class
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: {} = {}(client)",
|
||||
field_name_py, inline_class, inline_class
|
||||
field_name_py, child.inline_type_name, child.inline_type_name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
panic!(
|
||||
"Field '{}' has no matching index pattern. All metrics must be indexed.",
|
||||
field.name
|
||||
// Use pattern class with metric base
|
||||
let py_type = metadata.resolve_tree_field_type(
|
||||
&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!(
|
||||
output,
|
||||
r#"use std::sync::Arc;
|
||||
use std::ops::{{Bound, RangeBounds}};
|
||||
use serde::de::DeserializeOwned;
|
||||
pub use brk_cohort::*;
|
||||
pub use brk_types::*;
|
||||
@@ -193,21 +194,30 @@ impl EndpointConfig {{
|
||||
|
||||
/// 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
|
||||
/// ```ignore
|
||||
/// // Get all data
|
||||
/// endpoint.json()?;
|
||||
/// // Fetch all data
|
||||
/// let data = endpoint.fetch()?;
|
||||
///
|
||||
/// // Get last 10 points
|
||||
/// endpoint.last(10).json()?;
|
||||
/// // Get single item at index 5
|
||||
/// let data = endpoint.get(5).fetch()?;
|
||||
///
|
||||
/// // Get first 10 using range
|
||||
/// let data = endpoint.range(..10).fetch()?;
|
||||
///
|
||||
/// // Get range [100, 200)
|
||||
/// endpoint.range(100, 200).json()?;
|
||||
/// let data = endpoint.range(100..200).fetch()?;
|
||||
///
|
||||
/// // Get 10 points starting from position 100
|
||||
/// endpoint.from(100).take(10).json()?;
|
||||
/// // Get first 10 (convenience)
|
||||
/// 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> {{
|
||||
config: EndpointConfig,
|
||||
@@ -219,44 +229,59 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
|
||||
Self {{ config: EndpointConfig::new(client, name, index), _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Fetch the first n data points.
|
||||
pub fn first(mut self, n: u64) -> RangeBuilder<T> {{
|
||||
self.config.end = Some(n as i64);
|
||||
/// Select a specific index position.
|
||||
pub fn get(mut self, index: usize) -> SingleItemBuilder<T> {{
|
||||
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 }}
|
||||
}}
|
||||
|
||||
/// Fetch the last n data points.
|
||||
pub fn last(mut self, n: u64) -> RangeBuilder<T> {{
|
||||
/// Take the first n items.
|
||||
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));
|
||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Set an explicit range [start, end).
|
||||
pub fn range(mut self, start: i64, end: i64) -> RangeBuilder<T> {{
|
||||
self.config.start = Some(start);
|
||||
self.config.end = Some(end);
|
||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
/// Skip the first n items. Chain with `take(n)` to get a range.
|
||||
pub fn skip(mut self, n: usize) -> SkippedBuilder<T> {{
|
||||
self.config.start = Some(n as i64);
|
||||
SkippedBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Set the start position. Chain with `take(n)` or `to(end)`.
|
||||
pub fn from(mut self, start: i64) -> FromBuilder<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>> {{
|
||||
/// Fetch all data as parsed JSON.
|
||||
pub fn fetch(self) -> Result<MetricData<T>> {{
|
||||
self.config.get_json(None)
|
||||
}}
|
||||
|
||||
/// Execute the query and return CSV data as a string (all data).
|
||||
pub fn csv(self) -> Result<String> {{
|
||||
/// Fetch all data as CSV string.
|
||||
pub fn fetch_csv(self) -> Result<String> {{
|
||||
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)`.
|
||||
pub struct FromBuilder<T> {{
|
||||
/// Builder for single item access.
|
||||
pub struct SingleItemBuilder<T> {{
|
||||
config: EndpointConfig,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}}
|
||||
|
||||
impl<T: DeserializeOwned> FromBuilder<T> {{
|
||||
/// Take n items from the start position.
|
||||
pub fn take(mut self, n: u64) -> RangeBuilder<T> {{
|
||||
impl<T: DeserializeOwned> SingleItemBuilder<T> {{
|
||||
/// Fetch the single item.
|
||||
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);
|
||||
self.config.end = Some(start + n as i64);
|
||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Set the end position.
|
||||
pub fn to(mut self, end: i64) -> RangeBuilder<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>> {{
|
||||
/// Fetch from the skipped position to the end.
|
||||
pub fn fetch(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> {{
|
||||
/// Fetch from the skipped position to the end as CSV.
|
||||
pub fn fetch_csv(self) -> Result<String> {{
|
||||
self.config.get_text(Some("csv"))
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Builder after calling `to(end)`. Can chain with `takeLast(n)` or `from(start)`.
|
||||
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()`.
|
||||
/// Builder with range fully specified.
|
||||
pub struct RangeBuilder<T> {{
|
||||
config: EndpointConfig,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}}
|
||||
|
||||
impl<T: DeserializeOwned> RangeBuilder<T> {{
|
||||
/// Execute the query and return parsed JSON data.
|
||||
pub fn json(self) -> Result<MetricData<T>> {{
|
||||
/// Fetch the range as parsed JSON.
|
||||
pub fn fetch(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> {{
|
||||
/// Fetch the range as CSV string.
|
||||
pub fn fetch_csv(self) -> Result<String> {{
|
||||
self.config.get_text(Some("csv"))
|
||||
}}
|
||||
}}
|
||||
|
||||
@@ -6,9 +6,8 @@ use std::fmt::Write;
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax, child_type_name,
|
||||
generate_leaf_field, generate_tree_node_field, get_node_fields, get_pattern_instance_base,
|
||||
prepare_tree_node, to_snake_case,
|
||||
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax,
|
||||
generate_leaf_field, generate_tree_node_field, prepare_tree_node, to_snake_case,
|
||||
};
|
||||
|
||||
/// Generate tree structs.
|
||||
@@ -39,25 +38,29 @@ fn generate_tree_node(
|
||||
return;
|
||||
};
|
||||
|
||||
// Generate struct definition
|
||||
writeln!(output, "/// Metrics tree node.").unwrap();
|
||||
writeln!(output, "pub struct {} {{", name).unwrap();
|
||||
|
||||
for ((field, child_fields), (child_name, _)) in
|
||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
||||
{
|
||||
let field_name = to_snake_case(&field.name);
|
||||
let type_annotation = metadata.resolve_tree_field_type(
|
||||
field,
|
||||
child_fields.as_deref(),
|
||||
name,
|
||||
child_name,
|
||||
GenericSyntax::RUST,
|
||||
);
|
||||
for child in &ctx.children {
|
||||
let field_name = to_snake_case(child.name);
|
||||
let type_annotation = if child.should_inline {
|
||||
child.inline_type_name.clone()
|
||||
} else {
|
||||
metadata.resolve_tree_field_type(
|
||||
&child.field,
|
||||
child.child_fields.as_deref(),
|
||||
name,
|
||||
child.name,
|
||||
GenericSyntax::RUST,
|
||||
)
|
||||
};
|
||||
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
// Generate impl block
|
||||
writeln!(output, "impl {} {{", name).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
@@ -67,53 +70,40 @@ fn generate_tree_node(
|
||||
writeln!(output, " Self {{").unwrap();
|
||||
|
||||
let syntax = RustSyntax;
|
||||
for ((field_info, child_fields), (child_name, child_node)) in
|
||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
||||
{
|
||||
let field_name = to_snake_case(&field_info.name);
|
||||
for child in &ctx.children {
|
||||
let field_name = to_snake_case(child.name);
|
||||
|
||||
// Check if this is a pattern type and if it's parameterizable
|
||||
let is_parameterizable = child_fields
|
||||
.as_ref()
|
||||
.is_some_and(|cf| metadata.is_parameterizable_fields(cf));
|
||||
|
||||
if metadata.is_pattern_type(&field_info.rust_type) && is_parameterizable {
|
||||
// Parameterizable pattern: use pattern constructor with metric base
|
||||
let pattern_base = get_pattern_instance_base(child_node);
|
||||
generate_tree_node_field(
|
||||
output,
|
||||
&syntax,
|
||||
field_info,
|
||||
metadata,
|
||||
" ",
|
||||
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));
|
||||
if child.is_leaf {
|
||||
if let TreeNode::Leaf(leaf) = child.node {
|
||||
generate_leaf_field(
|
||||
output,
|
||||
&syntax,
|
||||
"client.clone()",
|
||||
child.name,
|
||||
leaf,
|
||||
metadata,
|
||||
" ",
|
||||
);
|
||||
}
|
||||
} else if child.should_inline {
|
||||
// Inline struct
|
||||
let path_expr = syntax.path_expr("base_path", &format!("_{}", child.name));
|
||||
writeln!(
|
||||
output,
|
||||
" {}: {}::new(client.clone(), {}),",
|
||||
field_name, child_struct, path_expr
|
||||
field_name, child.inline_type_name, path_expr
|
||||
)
|
||||
.unwrap();
|
||||
} else if let TreeNode::Leaf(leaf) = child_node {
|
||||
// Leaf field - use shared helper
|
||||
generate_leaf_field(
|
||||
} else {
|
||||
// Use pattern constructor
|
||||
generate_tree_node_field(
|
||||
output,
|
||||
&syntax,
|
||||
"client.clone()",
|
||||
child_name,
|
||||
leaf,
|
||||
&child.field,
|
||||
metadata,
|
||||
" ",
|
||||
);
|
||||
} else {
|
||||
panic!(
|
||||
"Field '{}' is a leaf with no TreeNode::Leaf. This shouldn't happen.",
|
||||
field_info.name
|
||||
child.name,
|
||||
Some(&child.base_result.base),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -122,21 +112,17 @@ fn generate_tree_node(
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
for (child_name, child_node) in ctx.children {
|
||||
if let TreeNode::Branch(grandchildren) = child_node {
|
||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
||||
// Generate child struct if no pattern match OR pattern is not parameterizable
|
||||
if !metadata.is_parameterizable_fields(&child_fields) {
|
||||
let child_struct = child_type_name(name, child_name);
|
||||
generate_tree_node(
|
||||
output,
|
||||
&child_struct,
|
||||
child_node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
// Generate child structs
|
||||
for child in &ctx.children {
|
||||
if child.should_inline {
|
||||
generate_tree_node(
|
||||
output,
|
||||
&child.inline_type_name,
|
||||
child.node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ brk_rpc = { workspace = true }
|
||||
brk_server = { workspace = true }
|
||||
clap = { version = "4.5.54", features = ["derive", "string"] }
|
||||
color-eyre = { workspace = true }
|
||||
importmap = "0.1.2"
|
||||
importmap = "0.1.3"
|
||||
# importmap = { path = "../../../importmap" }
|
||||
tracing = { workspace = true }
|
||||
minreq = { workspace = true }
|
||||
|
||||
@@ -17,6 +17,7 @@ use brk_mempool::Mempool;
|
||||
use brk_query::AsyncQuery;
|
||||
use brk_reader::Reader;
|
||||
use brk_server::{Server, VERSION};
|
||||
use importmap::ImportMap;
|
||||
use tracing::info;
|
||||
use vecdb::Exit;
|
||||
|
||||
@@ -85,25 +86,28 @@ pub fn run() -> color_eyre::Result<()> {
|
||||
let data_path = config.brkdir();
|
||||
|
||||
let future = async move {
|
||||
let bundle_path = if website.is_some() {
|
||||
// Try to find local dev directories - check cwd and parent directories
|
||||
let find_dev_dirs = || -> Option<(PathBuf, PathBuf)> {
|
||||
let mut dir = std::env::current_dir().ok()?;
|
||||
loop {
|
||||
let websites = dir.join("websites");
|
||||
let modules = dir.join("modules");
|
||||
if websites.exists() && modules.exists() {
|
||||
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();
|
||||
// Try to find local dev directories - check cwd and parent directories
|
||||
let find_dev_dirs = || -> Option<(PathBuf, PathBuf)> {
|
||||
let mut dir = std::env::current_dir().ok()?;
|
||||
loop {
|
||||
let websites = dir.join("websites");
|
||||
let modules = dir.join("modules");
|
||||
if websites.exists() && modules.exists() {
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
} else {
|
||||
let downloaded_brk_path = downloads_path.join(format!("brk-{VERSION}"));
|
||||
@@ -133,19 +137,28 @@ pub fn run() -> color_eyre::Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
// Generate import map for cache busting
|
||||
// Generate import map for cache busting (disabled in dev mode)
|
||||
if let Some(ref path) = bundle_path {
|
||||
match importmap::ImportMap::scan(path, "") {
|
||||
Ok(map) => {
|
||||
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);
|
||||
info!("Updated importmap in index.html");
|
||||
let map = if is_dev {
|
||||
ImportMap::empty()
|
||||
} else {
|
||||
match ImportMap::scan(path, "") {
|
||||
Ok(map) => map,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to generate importmap: {e}");
|
||||
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
|
||||
// Using new idiomatic API: last(3).fetch()
|
||||
let price_close = client
|
||||
.metrics()
|
||||
.price
|
||||
@@ -22,8 +23,8 @@ fn main() -> brk_client::Result<()> {
|
||||
.close
|
||||
.by
|
||||
.dateindex()
|
||||
.from(-3)
|
||||
.json()?;
|
||||
.last(3)
|
||||
.fetch()?;
|
||||
println!("Last 3 price close values: {:?}", price_close);
|
||||
|
||||
// Fetch block data
|
||||
@@ -35,12 +36,11 @@ fn main() -> brk_client::Result<()> {
|
||||
.sum
|
||||
.by
|
||||
.dateindex()
|
||||
.from(-3)
|
||||
.json()?;
|
||||
.last(3)
|
||||
.fetch()?;
|
||||
println!("Last 3 block count values: {:?}", block_count);
|
||||
|
||||
// Fetch supply data
|
||||
//
|
||||
dbg!(
|
||||
client
|
||||
.metrics()
|
||||
@@ -58,8 +58,8 @@ fn main() -> brk_client::Result<()> {
|
||||
.bitcoin
|
||||
.by
|
||||
.dateindex()
|
||||
.from(-3)
|
||||
.csv()?;
|
||||
.last(3)
|
||||
.fetch_csv()?;
|
||||
println!("Last 3 circulating supply values: {:?}", circulating);
|
||||
|
||||
// Using generic metric fetching
|
||||
|
||||
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_error::Result;
|
||||
use brk_fetcher::Fetcher;
|
||||
use brk_indexer::Indexer;
|
||||
use vecdb::{AnyStoredVec, Exit};
|
||||
use vecdb::{AnySerializableVec, AnyVec, Exit};
|
||||
|
||||
pub fn main() -> Result<()> {
|
||||
// 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)?;
|
||||
|
||||
let outputs_dir = Path::new(&env::var("HOME").unwrap()).join(".brk");
|
||||
// let outputs_dir = Path::new("../../_outputs");
|
||||
|
||||
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 _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(())
|
||||
}
|
||||
|
||||
@@ -137,6 +137,18 @@ pub enum 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
|
||||
/// 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).
|
||||
|
||||
@@ -47,27 +47,26 @@ impl Indexer {
|
||||
let indexed_path = outputs_dir.join("indexed");
|
||||
|
||||
let try_import = || -> Result<Self> {
|
||||
let (vecs, stores) = thread::scope(|s| -> Result<_> {
|
||||
let vecs = s.spawn(|| -> Result<_> {
|
||||
let i = Instant::now();
|
||||
let vecs = Vecs::forced_import(&indexed_path, VERSION)?;
|
||||
info!("Imported vecs in {:?}", i.elapsed());
|
||||
Ok(vecs)
|
||||
});
|
||||
let i = Instant::now();
|
||||
let vecs = Vecs::forced_import(&indexed_path, VERSION)?;
|
||||
info!("Imported vecs in {:?}", i.elapsed());
|
||||
|
||||
let i = Instant::now();
|
||||
let stores = Stores::forced_import(&indexed_path, VERSION)?;
|
||||
info!("Imported stores in {:?}", i.elapsed());
|
||||
|
||||
Ok((vecs.join().unwrap()?, stores))
|
||||
})?;
|
||||
let i = Instant::now();
|
||||
let stores = Stores::forced_import(&indexed_path, VERSION)?;
|
||||
info!("Imported stores in {:?}", i.elapsed());
|
||||
|
||||
Ok(Self { vecs, stores })
|
||||
};
|
||||
|
||||
match try_import() {
|
||||
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");
|
||||
fs::remove_dir_all(&indexed_path)?;
|
||||
Self::forced_import_inner(outputs_dir, false)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,10 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/bitcoinresearchkit/brk/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node tests/basic.js",
|
||||
"test:tree": "node tests/tree.js"
|
||||
},
|
||||
"description": "BRK JavaScript client",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
||||
@@ -1,5 +1,74 @@
|
||||
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.
|
||||
* @param {object} obj
|
||||
* @param {Record<string, any>} obj
|
||||
* @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 = "") {
|
||||
const metrics = [];
|
||||
@@ -21,10 +21,10 @@ function getAllMetrics(obj, path = "") {
|
||||
|
||||
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") {
|
||||
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) {
|
||||
metrics.push({ path: currentPath, metric: attr, indexes });
|
||||
@@ -41,54 +41,38 @@ function getAllMetrics(obj, path = "") {
|
||||
}
|
||||
|
||||
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`);
|
||||
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
const errors = [];
|
||||
|
||||
for (const { path, metric, indexes } of metrics) {
|
||||
for (const idxName of indexes) {
|
||||
try {
|
||||
const endpoint = metric.by[idxName]();
|
||||
const res = await endpoint.range(-3);
|
||||
const endpoint = metric.by[idxName];
|
||||
const res = await endpoint.last(1);
|
||||
const count = res.data.length;
|
||||
if (count !== 3) {
|
||||
failed++;
|
||||
const errorMsg = `FAIL: ${path}.by.${idxName}() -> expected 3, got ${count}`;
|
||||
errors.push(errorMsg);
|
||||
console.log(errorMsg);
|
||||
} else {
|
||||
success++;
|
||||
console.log(`OK: ${path}.by.${idxName}() -> ${count} items`);
|
||||
if (count !== 1) {
|
||||
console.log(
|
||||
`FAIL: ${path}.by.${idxName} -> expected 1, got ${count}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
success++;
|
||||
console.log(`OK: ${path}.by.${idxName} -> ${count} items`);
|
||||
} catch (e) {
|
||||
failed++;
|
||||
const errorMsg = `FAIL: ${path}.by.${idxName}() -> ${e.message}`;
|
||||
errors.push(errorMsg);
|
||||
console.log(errorMsg);
|
||||
console.log(
|
||||
`FAIL: ${path}.by.${idxName} -> ${e instanceof Error ? e.message : e}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n=== Results ===`);
|
||||
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();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,29 +36,30 @@ def test_fetch_csv_metric():
|
||||
|
||||
def test_fetch_typed_metric():
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
d = (
|
||||
client.metrics.market.dca.period_lump_sum_stack._10y.dollars.by.dateindex()
|
||||
.from_(-10)
|
||||
.json()
|
||||
.tail(10)
|
||||
.fetch()
|
||||
)
|
||||
print(d)
|
||||
e = (
|
||||
client.metrics.market.dca.class_average_price._2017.by.dateindex()
|
||||
.from_(-10)
|
||||
.json()
|
||||
.tail(10)
|
||||
.fetch()
|
||||
)
|
||||
print(e)
|
||||
f = (
|
||||
client.metrics.distribution.address_cohorts.amount_range._10k_sats_to_100k_sats.activity.sent.dollars.cumulative.by.dateindex()
|
||||
.from_(-10)
|
||||
.json()
|
||||
.tail(10)
|
||||
.fetch()
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -41,7 +41,7 @@ def test_all_endpoints():
|
||||
"""Test fetching last 3 values from all metric endpoints."""
|
||||
client = BrkClient("http://localhost:3110")
|
||||
|
||||
metrics = get_all_metrics(client.tree)
|
||||
metrics = get_all_metrics(client.metrics)
|
||||
print(f"\nFound {len(metrics)} metrics")
|
||||
|
||||
success = 0
|
||||
@@ -53,7 +53,8 @@ def test_all_endpoints():
|
||||
try:
|
||||
by = metric.by
|
||||
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"])
|
||||
if count != 3:
|
||||
failed += 1
|
||||
|
||||
@@ -1515,148 +1515,7 @@
|
||||
</style>
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
<!-- ------- -->
|
||||
|
||||
@@ -19,6 +19,7 @@ import { throttle } from "../utils/timing.js";
|
||||
import { serdeBool } from "../utils/serde.js";
|
||||
import { stringToId } from "../utils/format.js";
|
||||
import { style } from "../utils/elements.js";
|
||||
import { resources } from "../resources.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} Valued
|
||||
@@ -70,20 +71,18 @@ const lineWidth = /** @type {any} */ (1.5);
|
||||
* @param {HTMLElement} args.parent
|
||||
* @param {Signals} args.signals
|
||||
* @param {Colors} args.colors
|
||||
* @param {Resources} args.resources
|
||||
* @param {BrkClient} args.brk
|
||||
* @param {Accessor<ChartableIndex>} args.index
|
||||
* @param {((unknownTimeScaleCallback: VoidFunction) => void)} [args.timeScaleSetCallback]
|
||||
* @param {true} [args.fitContent]
|
||||
* @param {{unit: Unit; blueprints: AnySeriesBlueprint[]}[]} [args.config]
|
||||
*/
|
||||
function createChartElement({
|
||||
export function createChartElement({
|
||||
parent,
|
||||
signals,
|
||||
colors,
|
||||
id: chartId,
|
||||
index,
|
||||
resources,
|
||||
brk,
|
||||
timeScaleSetCallback,
|
||||
fitContent,
|
||||
@@ -1076,5 +1075,3 @@ function numberToUSFormat(value, digits, options) {
|
||||
* @typedef {typeof createChartElement} CreateChartElement
|
||||
* @typedef {ReturnType<createChartElement>} Chart
|
||||
*/
|
||||
|
||||
export default { createChartElement };
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @import { Signal, Signals, Accessor } from "./signals.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'
|
||||
*
|
||||
@@ -28,23 +28,91 @@
|
||||
// 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
|
||||
*
|
||||
* 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 {Indexes[number]} IndexName
|
||||
* @typedef {InstanceType<typeof BrkClient>["POOL_ID_TO_POOL_NAME"]} PoolIdToPoolName
|
||||
* @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
|
||||
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} UtxoCohortPattern
|
||||
* @typedef {AddressAmountPattern} AddressCohortPattern
|
||||
* @typedef {UtxoCohortPattern | AddressCohortPattern} CohortPattern
|
||||
*
|
||||
* Relative pattern capability types
|
||||
* @typedef {RelativePattern | RelativePattern5} RelativeWithMarketCap
|
||||
* @typedef {RelativePattern2 | RelativePattern5} RelativeWithOwnMarketCap
|
||||
* @typedef {RelativePattern2 | RelativePattern5 | AllRelativePattern} RelativeWithOwnPnl
|
||||
* @typedef {GlobalRelativePattern | FullRelativePattern} RelativeWithMarketCap
|
||||
* @typedef {OwnRelativePattern | FullRelativePattern} RelativeWithOwnMarketCap
|
||||
* @typedef {OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithOwnPnl
|
||||
*
|
||||
* Capability-based pattern groupings (patterns that have specific properties)
|
||||
* @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)),
|
||||
]),
|
||||
)
|
||||
);
|
||||
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 })
|
||||
* @param {Colors} colors
|
||||
* @param {SizePattern} pattern
|
||||
* @param {AnyStatsPattern} pattern
|
||||
* @param {string} title
|
||||
* @param {Color} [color]
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
@@ -241,7 +241,7 @@ export function fromBlockSize(colors, pattern, title, color) {
|
||||
/**
|
||||
* Create series from a SizePattern ({ average, sum, cumulative, min, max, percentiles })
|
||||
* @param {Colors} colors
|
||||
* @param {SizePattern} pattern
|
||||
* @param {AnyStatsPattern} pattern
|
||||
* @param {string} title
|
||||
* @param {Unit} unit
|
||||
* @returns {AnyFetchedSeriesBlueprint[]}
|
||||
|
||||
@@ -238,8 +238,8 @@
|
||||
* @property {HistogramSeriesFn} histogram
|
||||
* @property {(pattern: BlockCountPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockCount
|
||||
* @property {(pattern: FullnessPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBitcoin
|
||||
* @property {(pattern: SizePattern, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockSize
|
||||
* @property {(pattern: SizePattern, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromSizePattern
|
||||
* @property {(pattern: AnyStatsPattern, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockSize
|
||||
* @property {(pattern: AnyStatsPattern, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromSizePattern
|
||||
* @property {(pattern: FullnessPattern<any>, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromFullnessPattern
|
||||
* @property {(pattern: FeeRatePattern<any>, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromFeeRatePattern
|
||||
* @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 { throttle } from "../../utils/timing.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 ONE_BTC_IN_SATS = 100_000_000;
|
||||
@@ -22,20 +25,12 @@ const CANDLE = "candle";
|
||||
/**
|
||||
* @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,
|
||||
createChartElement,
|
||||
option,
|
||||
signals,
|
||||
webSockets,
|
||||
resources,
|
||||
brk,
|
||||
}) {
|
||||
chartElement.append(createShadow("left"));
|
||||
@@ -44,10 +39,7 @@ export function init({
|
||||
const { headerElement, headingElement } = createHeader();
|
||||
chartElement.append(headerElement);
|
||||
|
||||
const { index, fieldset } = createIndexSelector({
|
||||
option,
|
||||
signals,
|
||||
});
|
||||
const { index, fieldset } = createIndexSelector(option);
|
||||
|
||||
const TIMERANGE_LS_KEY = signals.createMemo(
|
||||
() => `chart-timerange-${index()}`,
|
||||
@@ -77,7 +69,6 @@ export function init({
|
||||
signals,
|
||||
colors,
|
||||
id: "charts",
|
||||
resources,
|
||||
brk,
|
||||
index,
|
||||
timeScaleSetCallback: (unknownTimeScaleCallback) => {
|
||||
@@ -525,11 +516,9 @@ export function init({
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Accessor<ChartOption>} args.option
|
||||
* @param {Signals} args.signals
|
||||
* @param {Accessor<ChartOption>} option
|
||||
*/
|
||||
function createIndexSelector({ option, signals }) {
|
||||
function createIndexSelector(option) {
|
||||
const choices_ = /** @satisfies {ChartableIndexName[]} */ ([
|
||||
"timestamp",
|
||||
"date",
|
||||
|
||||
@@ -1,25 +1,7 @@
|
||||
import { randomFromArray } from "../utils/array.js";
|
||||
import { explorerElement } from "../utils/elements.js";
|
||||
|
||||
/**
|
||||
* @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,
|
||||
}) {
|
||||
export function init() {
|
||||
const chain = window.document.createElement("div");
|
||||
chain.id = "chain";
|
||||
explorerElement.append(chain);
|
||||
|
||||
@@ -18,15 +18,15 @@ import {
|
||||
numberToUSNumber,
|
||||
} from "../utils/format.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 {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
|
||||
* @property {string} name
|
||||
|
||||
@@ -6,14 +6,7 @@ import { tableElement } from "../utils/elements.js";
|
||||
import { serdeMetrics, serdeString } from "../utils/serde.js";
|
||||
import { resetParams } from "../utils/url.js";
|
||||
|
||||
/**
|
||||
* @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 }) {
|
||||
export function init() {
|
||||
tableElement.innerHTML = "wip, will hopefuly be back soon, sorry !";
|
||||
|
||||
// const parent = tableElement;
|
||||
|
||||
@@ -25,105 +25,106 @@
|
||||
/** @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();
|
||||
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));
|
||||
|
||||
/**
|
||||
* Create a generic reactive resource wrapper for any async fetcher
|
||||
* @template T
|
||||
* @template {any[]} Args
|
||||
* @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();
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
/**
|
||||
* Get or create range state
|
||||
* @param {number} [from=-10000]
|
||||
* @param {number} [to]
|
||||
* @returns {RangeState<T>}
|
||||
* @param {Args} args
|
||||
*/
|
||||
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} [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 };
|
||||
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) {
|
||||
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) {
|
||||
/**
|
||||
* @template T
|
||||
* @param {(callback: (value: T) => void) => WebSocket} creator
|
||||
*/
|
||||
function createWebsocket(creator) {
|
||||
let ws = /** @type {WebSocket | null} */ (null);
|
||||
function createWebsocket(creator) {
|
||||
let ws = /** @type {WebSocket | null} */ (null);
|
||||
|
||||
const live = signals.createSignal(false);
|
||||
const latest = signals.createSignal(/** @type {T | null} */ (null));
|
||||
const live = signals.createSignal(false);
|
||||
const latest = signals.createSignal(/** @type {T | null} */ (null));
|
||||
|
||||
function reinitWebSocket() {
|
||||
if (!ws || ws.readyState === ws.CLOSED) {
|
||||
console.log("ws: reinit");
|
||||
resource.open();
|
||||
}
|
||||
function reinitWebSocket() {
|
||||
if (!ws || ws.readyState === ws.CLOSED) {
|
||||
console.log("ws: reinit");
|
||||
resource.open();
|
||||
}
|
||||
}
|
||||
|
||||
function reinitWebSocketIfDocumentNotHidden() {
|
||||
!window.document.hidden && reinitWebSocket();
|
||||
}
|
||||
function reinitWebSocketIfDocumentNotHidden() {
|
||||
!window.document.hidden && reinitWebSocket();
|
||||
}
|
||||
|
||||
const resource = {
|
||||
live,
|
||||
latest,
|
||||
open() {
|
||||
ws = creator((value) => latest.set(() => value));
|
||||
const resource = {
|
||||
live,
|
||||
latest,
|
||||
open() {
|
||||
ws = creator((value) => latest.set(() => value));
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
console.log("ws: open");
|
||||
live.set(true);
|
||||
});
|
||||
ws.addEventListener("open", () => {
|
||||
console.log("ws: open");
|
||||
live.set(true);
|
||||
});
|
||||
|
||||
ws.addEventListener("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);
|
||||
ws.addEventListener("close", () => {
|
||||
console.log("ws: close");
|
||||
live.set(false);
|
||||
ws = null;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,
|
||||
},
|
||||
}),
|
||||
window.document.addEventListener(
|
||||
"visibilitychange",
|
||||
reinitWebSocketIfDocumentNotHidden,
|
||||
);
|
||||
});
|
||||
|
||||
ws.addEventListener("message", (message) => {
|
||||
const result = JSON.parse(message.data);
|
||||
window.document.addEventListener("online", reinitWebSocket);
|
||||
},
|
||||
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} */
|
||||
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;
|
||||
}
|
||||
|
||||
/** @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
|
||||
}`;
|
||||
}
|
||||
ws.addEventListener("open", () => {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
method: "subscribe",
|
||||
params: {
|
||||
channel: "ohlc",
|
||||
symbol: ["BTC/USD"],
|
||||
interval: 1440,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
kraken1dCandle,
|
||||
};
|
||||
ws.addEventListener("message", (message) => {
|
||||
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