global: snapshot

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