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

24
Cargo.lock generated
View File

@@ -1954,9 +1954,9 @@ dependencies = [
[[package]]
name = "importmap"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d13d6361899f14d58146b6214c07e63cda8270c3ef3b8c30626f8e20e6766eb"
checksum = "a45c4c5e8b4d0a6aed90f3a14125bd3af3bca73d49d44e8f8b764311903b5213"
dependencies = [
"rapidhash",
"serde",
@@ -2684,7 +2684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"rand_core 0.9.4",
]
[[package]]
@@ -2704,7 +2704,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
"rand_core 0.9.4",
]
[[package]]
@@ -2718,9 +2718,9 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.9.3"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
checksum = "4f1b3bc831f92381018fd9c6350b917c7b21f1eed35a65a51900e0e55a3d7afa"
dependencies = [
"getrandom 0.3.4",
]
@@ -2745,9 +2745,9 @@ dependencies = [
[[package]]
name = "rawdb"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1eb09ba02f9467845fde4a1fadb317721025f2b836f22a5a7d3567c9e100875"
checksum = "eb8999fc8f80521c13091f98e1a24bde2448b29c0637f008dc470ed84d58f703"
dependencies = [
"libc",
"log",
@@ -3689,9 +3689,9 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
[[package]]
name = "vecdb"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b02690e7c013257b959b482fac78e90f73764efa8a57551e1e67a06ad7ab4"
checksum = "1a31985bee527adda1601eab9c72de0a567be83a08e80e646b79afb77ab14970"
dependencies = [
"ctrlc",
"log",
@@ -3710,9 +3710,9 @@ dependencies = [
[[package]]
name = "vecdb_derive"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21339c58345d1a422c2574b1114b3cd862900a9196421dc7acf43aca48c288bb"
checksum = "61716cbeb322dcc9838cea11b533cea278f9592b2ef2ce520595849833cfee1c"
dependencies = [
"quote",
"syn",

View File

@@ -79,7 +79,7 @@ serde_json = { version = "1.0.149", features = ["float_roundtrip", "preserve_ord
smallvec = "1.15.1"
tokio = { version = "1.49.0", features = ["rt-multi-thread"] }
tracing = { version = "0.1", default-features = false, features = ["std"] }
vecdb = { version = "0.5.8", features = ["derive", "serde_json", "pco", "schemars"] }
vecdb = { version = "0.5.9", features = ["derive", "serde_json", "pco", "schemars"] }
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
[workspace.metadata.release]

View File

@@ -458,4 +458,33 @@ mod tests {
Some(&FieldNamePosition::Append("_cumulative".to_string()))
);
}
#[test]
fn test_analyze_pattern_level_no_base_field() {
// When there's no base field (like block_weight which has no block_weight metric),
// only suffixed metrics like block_weight_average, block_weight_sum, etc.
// Base should still be "block_weight"
let children = vec![
("average".to_string(), "block_weight_average".to_string()),
("sum".to_string(), "block_weight_sum".to_string()),
(
"cumulative".to_string(),
"block_weight_cumulative".to_string(),
),
("max".to_string(), "block_weight_max".to_string()),
("min".to_string(), "block_weight_min".to_string()),
];
let analysis = analyze_pattern_level(&children);
assert!(matches!(analysis.common, CommonDenominator::Prefix(_)));
assert_eq!(analysis.base, "block_weight");
assert_eq!(
analysis.field_positions.get("average"),
Some(&FieldNamePosition::Append("_average".to_string()))
);
assert_eq!(
analysis.field_positions.get("sum"),
Some(&FieldNamePosition::Append("_sum".to_string()))
);
}
}

View File

@@ -7,7 +7,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
use brk_types::{Index, TreeNode, extract_json_type};
use crate::{IndexSetPattern, PatternField, analysis::names::analyze_pattern_level, child_type_name};
use crate::{IndexSetPattern, PatternField, analysis::names::{analyze_pattern_level, CommonDenominator}, child_type_name};
/// Get the first leaf name from a tree node.
pub fn get_first_leaf_name(node: &TreeNode) -> Option<String> {
@@ -124,16 +124,81 @@ fn collect_indexes_from_tree(
}
}
/// Result of analyzing a pattern instance's base.
#[derive(Debug, Clone)]
pub struct PatternBaseResult {
/// The computed base name for the pattern.
pub base: String,
/// Whether an outlier child was excluded to find the pattern.
/// If true, pattern factory should not be used.
pub has_outlier: bool,
}
impl PatternBaseResult {
/// Returns true if an inline type should be generated instead of using a pattern factory.
///
/// This is the case when:
/// - The child fields don't match a parameterizable pattern, OR
/// - An outlier was detected during pattern analysis
pub fn should_inline(&self, is_parameterizable: bool) -> bool {
!is_parameterizable || self.has_outlier
}
}
/// Get the metric base for a pattern instance by analyzing direct children.
///
/// Uses field names and first leaf names from direct children to determine
/// the common base via `analyze_pattern_level`.
pub fn get_pattern_instance_base(node: &TreeNode) -> String {
///
/// If the initial analysis fails to find a common pattern, it tries excluding
/// each child one at a time to detect outliers (e.g., a mismatched "base" field
/// from indexer/computed tree merging).
///
/// Returns both the base and whether an outlier was detected.
pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
let child_names = get_direct_children_for_analysis(node);
if child_names.is_empty() {
return String::new();
return PatternBaseResult {
base: String::new(),
has_outlier: false,
};
}
let analysis = analyze_pattern_level(&child_names);
// If we found a common pattern, use it
if !matches!(analysis.common, CommonDenominator::None) {
return PatternBaseResult {
base: analysis.base,
has_outlier: false,
};
}
// If no common pattern found, try excluding each child one at a time
// to detect if there's a single outlier breaking the pattern.
if child_names.len() > 2 {
for i in 0..child_names.len() {
let filtered: Vec<_> = child_names
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, v)| v.clone())
.collect();
let filtered_analysis = analyze_pattern_level(&filtered);
if !matches!(filtered_analysis.common, CommonDenominator::None) {
return PatternBaseResult {
base: filtered_analysis.base,
has_outlier: true,
};
}
}
}
PatternBaseResult {
base: analysis.base,
has_outlier: false,
}
analyze_pattern_level(&child_names).base
}
/// Get (field_name, shortest_leaf_name) pairs for direct children of a branch node.
@@ -217,3 +282,93 @@ pub fn get_fields_with_child_info(
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use brk_types::{MetricLeaf, MetricLeafWithSchema, TreeNode};
use std::collections::BTreeMap;
fn make_leaf(name: &str) -> TreeNode {
let leaf = MetricLeaf {
name: name.to_string(),
kind: "TestType".to_string(),
indexes: BTreeSet::new(),
};
TreeNode::Leaf(MetricLeafWithSchema::new(leaf, serde_json::json!({})))
}
fn make_branch(children: Vec<(&str, TreeNode)>) -> TreeNode {
let map: BTreeMap<String, TreeNode> = children
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
TreeNode::Branch(map)
}
#[test]
fn test_get_pattern_instance_base_with_base_field() {
// Simulates vbytes tree: has base field with block_vbytes leaf
let tree = make_branch(vec![
("base", make_branch(vec![("dateindex", make_leaf("block_vbytes"))])),
("average", make_branch(vec![("dateindex", make_leaf("block_vbytes_average"))])),
("sum", make_branch(vec![("dateindex", make_leaf("block_vbytes_sum"))])),
]);
let result = get_pattern_instance_base(&tree);
assert_eq!(result.base, "block_vbytes");
assert!(!result.has_outlier);
}
#[test]
fn test_get_pattern_instance_base_without_base_field() {
// Simulates weight tree: NO base field, only suffixed metrics
let tree = make_branch(vec![
("average", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
("sum", make_branch(vec![("dateindex", make_leaf("block_weight_sum"))])),
("cumulative", make_branch(vec![("dateindex", make_leaf("block_weight_cumulative"))])),
("max", make_branch(vec![("dateindex", make_leaf("block_weight_max"))])),
("min", make_branch(vec![("dateindex", make_leaf("block_weight_min"))])),
]);
let result = get_pattern_instance_base(&tree);
assert_eq!(result.base, "block_weight");
assert!(!result.has_outlier);
}
#[test]
fn test_get_pattern_instance_base_with_duplicate_base_field() {
// What if there's a "base" field that points to the same leaf as "average"?
// This could happen if the tree generation creates a base field that shares leaves with average
let tree = make_branch(vec![
("base", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
("average", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
("sum", make_branch(vec![("dateindex", make_leaf("block_weight_sum"))])),
]);
let result = get_pattern_instance_base(&tree);
// Common prefix among all children is "block_weight_"
assert_eq!(result.base, "block_weight");
assert!(!result.has_outlier);
}
#[test]
fn test_get_pattern_instance_base_with_mismatched_base_name() {
// Simulates the actual bug: indexed tree's "base" field has name "weight"
// but computed tree's derived metrics use "block_weight_*" prefix.
// After tree merge, we get a base field with mismatched naming.
let tree = make_branch(vec![
("base", make_leaf("weight")), // Outlier - doesn't match pattern
("average", make_leaf("block_weight_average")),
("sum", make_leaf("block_weight_sum")),
("cumulative", make_leaf("block_weight_cumulative")),
("max", make_leaf("block_weight_max")),
("min", make_leaf("block_weight_min")),
]);
let result = get_pattern_instance_base(&tree);
// Should detect "weight" as outlier and find common prefix from others
assert_eq!(result.base, "block_weight");
assert!(result.has_outlier); // Pattern factory should NOT be used
}
}

View File

@@ -4,16 +4,35 @@ use std::collections::{HashMap, HashSet};
use brk_types::TreeNode;
use crate::{ClientMetadata, PatternField, get_fields_with_child_info};
use crate::{
ClientMetadata, PatternBaseResult, PatternField, child_type_name, get_fields_with_child_info,
get_pattern_instance_base,
};
/// Pre-computed context for a single child node.
pub struct ChildContext<'a> {
/// The child's field name in the tree.
pub name: &'a str,
/// The child node.
pub node: &'a TreeNode,
/// The field info for this child.
pub field: PatternField,
/// Child fields if this is a branch (for pattern lookup).
pub child_fields: Option<Vec<PatternField>>,
/// Pattern analysis result.
pub base_result: PatternBaseResult,
/// Whether this is a leaf node.
pub is_leaf: bool,
/// Whether to use an inline type instead of a pattern factory (only meaningful for branches).
pub should_inline: bool,
/// The type name to use for inline branches.
pub inline_type_name: String,
}
/// Context for generating a tree node, returned by `prepare_tree_node`.
pub struct TreeNodeContext<'a> {
/// The children of the branch node.
pub children: &'a std::collections::BTreeMap<String, TreeNode>,
/// Fields with optional child field info for generic pattern lookup.
pub fields_with_child_info: Vec<(PatternField, Option<Vec<PatternField>>)>,
/// Just the fields (for pattern lookup).
pub fields: Vec<PatternField>,
/// Pre-computed context for each child.
pub children: Vec<ChildContext<'a>>,
}
/// Prepare a tree node for generation.
@@ -26,20 +45,22 @@ pub fn prepare_tree_node<'a>(
metadata: &ClientMetadata,
generated: &mut HashSet<String>,
) -> Option<TreeNodeContext<'a>> {
let TreeNode::Branch(children) = node else {
let TreeNode::Branch(branch_children) = node else {
return None;
};
let fields_with_child_info = get_fields_with_child_info(children, name, pattern_lookup);
let fields_with_child_info = get_fields_with_child_info(branch_children, name, pattern_lookup);
let fields: Vec<PatternField> = fields_with_child_info
.iter()
.map(|(f, _)| f.clone())
.collect();
// Skip if this matches a parameterizable pattern
// Skip if this matches a parameterizable pattern AND has no outlier
let base_result = get_pattern_instance_base(node);
if let Some(pattern_name) = pattern_lookup.get(&fields)
&& pattern_name != name
&& metadata.is_parameterizable(pattern_name)
&& !base_result.has_outlier
{
return None;
}
@@ -50,9 +71,38 @@ pub fn prepare_tree_node<'a>(
}
generated.insert(name.to_string());
Some(TreeNodeContext {
children,
fields_with_child_info,
fields,
})
// Build child contexts with pre-computed decisions
let children: Vec<ChildContext<'a>> = branch_children
.iter()
.zip(fields_with_child_info)
.map(|((child_name, child_node), (field, child_fields))| {
let is_leaf = matches!(child_node, TreeNode::Leaf(_));
let base_result = get_pattern_instance_base(child_node);
let is_parameterizable = child_fields
.as_ref()
.is_some_and(|cf| metadata.is_parameterizable_fields(cf));
// should_inline is only meaningful for branches
let should_inline = !is_leaf && base_result.should_inline(is_parameterizable);
// Inline type name (only used when should_inline is true)
let inline_type_name = if should_inline {
child_type_name(name, child_name)
} else {
String::new()
};
ChildContext {
name: child_name,
node: child_node,
field,
child_fields,
base_result,
is_leaf,
should_inline,
inline_type_name,
}
})
.collect();
Some(TreeNodeContext { children })
}

View File

@@ -63,57 +63,65 @@ class BrkError extends Error {{
* @property {{number}} end - End index (exclusive)
* @property {{T[]}} data - The metric data
*/
/** @typedef {{MetricData<unknown>}} AnyMetricData */
/** @typedef {{MetricData<any>}} AnyMetricData */
/**
* Thenable interface for await support.
* @template T
* @typedef {{(onfulfilled?: (value: MetricData<T>) => MetricData<T>, onrejected?: (reason: Error) => never) => Promise<MetricData<T>>}} Thenable
*/
/**
* Metric endpoint builder. Callable (returns itself) so both .by.dateindex and .by.dateindex() work.
* @template T
* @typedef {{Object}} MetricEndpointBuilder
* @property {{(n: number) => RangeBuilder<T>}} first - Fetch first n data points
* @property {{(n: number) => RangeBuilder<T>}} last - Fetch last n data points
* @property {{(start: number, end: number) => RangeBuilder<T>}} range - Set explicit range [start, end)
* @property {{(start: number) => FromBuilder<T>}} from - Set start position, chain with take() or to()
* @property {{(end: number) => ToBuilder<T>}} to - Set end position, chain with takeLast() or from()
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON (all data)
* @property {{() => Promise<string>}} csv - Execute and return CSV (all data)
* @property {{(index: number) => SingleItemBuilder<T>}} get - Get single item at index
* @property {{(start?: number, end?: number) => RangeBuilder<T>}} slice - Slice like Array.slice
* @property {{(n: number) => RangeBuilder<T>}} first - Get first n items
* @property {{(n: number) => RangeBuilder<T>}} last - Get last n items
* @property {{(n: number) => SkippedBuilder<T>}} skip - Skip first n items, chain with take()
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch all data
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
* @property {{Thenable<T>}} then - Thenable (await endpoint)
* @property {{string}} path - The endpoint path
*/
/** @typedef {{MetricEndpointBuilder<unknown>}} AnyMetricEndpointBuilder */
/** @typedef {{MetricEndpointBuilder<any>}} AnyMetricEndpointBuilder */
/**
* @template T
* @typedef {{Object}} FromBuilder
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items from start position
* @property {{(end: number) => RangeBuilder<T>}} to - Set end position
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON
* @property {{() => Promise<string>}} csv - Execute and return CSV
* @typedef {{Object}} SingleItemBuilder
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the item
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/**
* @template T
* @typedef {{Object}} ToBuilder
* @property {{(n: number) => RangeBuilder<T>}} takeLast - Take last n items before end position
* @property {{(start: number) => RangeBuilder<T>}} from - Set start position
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON
* @property {{() => Promise<string>}} csv - Execute and return CSV
* @typedef {{Object}} SkippedBuilder
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items after skipped position
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch from skipped position to end
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/**
* @template T
* @typedef {{Object}} RangeBuilder
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON
* @property {{() => Promise<string>}} csv - Execute and return CSV
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the range
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/**
* @template T
* @typedef {{Object}} MetricPattern
* @property {{string}} name - The metric name
* @property {{Partial<Record<Index, MetricEndpointBuilder<T>>>}} by - Index endpoints (lazy getters)
* @property {{Readonly<Partial<Record<Index, MetricEndpointBuilder<T>>>>}} by - Index endpoints as lazy getters. Access via .by.dateindex or .by['dateindex']
* @property {{() => Index[]}} indexes - Get the list of available indexes
* @property {{(index: Index) => MetricEndpointBuilder<T>|undefined}} get - Get an endpoint for a specific index
*/
/** @typedef {{MetricPattern<unknown>}} AnyMetricPattern */
/** @typedef {{MetricPattern<any>}} AnyMetricPattern */
/**
* Create a metric endpoint builder with typestate pattern.
@@ -147,50 +155,46 @@ function _endpoint(client, name, index) {{
* @returns {{RangeBuilder<T>}}
*/
const rangeBuilder = (start, end) => ({{
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
return client.getJson(buildPath(start, end), onUpdate);
}},
csv() {{ return client.getText(buildPath(start, end, 'csv')); }},
fetch(onUpdate) {{ return client.getJson(buildPath(start, end), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(start, end, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
/**
* @param {{number}} index
* @returns {{SingleItemBuilder<T>}}
*/
const singleItemBuilder = (index) => ({{
fetch(onUpdate) {{ return client.getJson(buildPath(index, index + 1), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(index, index + 1, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
/**
* @param {{number}} start
* @returns {{FromBuilder<T>}}
* @returns {{SkippedBuilder<T>}}
*/
const fromBuilder = (start) => ({{
take(/** @type {{number}} */ n) {{ return rangeBuilder(start, start + n); }},
to(/** @type {{number}} */ end) {{ return rangeBuilder(start, end); }},
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
return client.getJson(buildPath(start, undefined), onUpdate);
}},
csv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
const skippedBuilder = (start) => ({{
take(n) {{ return rangeBuilder(start, start + n); }},
fetch(onUpdate) {{ return client.getJson(buildPath(start, undefined), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
/**
* @param {{number}} end
* @returns {{ToBuilder<T>}}
*/
const toBuilder = (end) => ({{
takeLast(/** @type {{number}} */ n) {{ return rangeBuilder(end - n, end); }},
from(/** @type {{number}} */ start) {{ return rangeBuilder(start, end); }},
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
return client.getJson(buildPath(undefined, end), onUpdate);
}},
csv() {{ return client.getText(buildPath(undefined, end, 'csv')); }},
}});
return {{
first(/** @type {{number}} */ n) {{ return rangeBuilder(undefined, n); }},
last(/** @type {{number}} */ n) {{ return rangeBuilder(-n, undefined); }},
range(/** @type {{number}} */ start, /** @type {{number}} */ end) {{ return rangeBuilder(start, end); }},
from(/** @type {{number}} */ start) {{ return fromBuilder(start); }},
to(/** @type {{number}} */ end) {{ return toBuilder(end); }},
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
return client.getJson(buildPath(), onUpdate);
}},
csv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
/** @type {{MetricEndpointBuilder<T>}} */
const endpoint = {{
get(index) {{ return singleItemBuilder(index); }},
slice(start, end) {{ return rangeBuilder(start, end); }},
first(n) {{ return rangeBuilder(undefined, n); }},
last(n) {{ return rangeBuilder(-n, undefined); }},
skip(n) {{ return skippedBuilder(n); }},
fetch(onUpdate) {{ return client.getJson(buildPath(), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
get path() {{ return p; }},
}};
return endpoint;
}}
/**
@@ -235,7 +239,7 @@ class BrkClientBase {{
const cachedJson = cachedRes ? await cachedRes.json() : null;
if (cachedJson) onUpdate?.(cachedJson);
if (!globalThis.navigator?.onLine) {{
if (globalThis.navigator?.onLine === false) {{
if (cachedJson) return cachedJson;
throw new BrkError('Offline and no cached data available');
}}
@@ -305,7 +309,11 @@ pub fn generate_static_constants(output: &mut String) {
fn instance_const_camel<T: Serialize>(output: &mut String, name: &str, value: &T) {
let json_value: Value = serde_json::to_value(value).unwrap();
let camel_value = camel_case_top_level_keys(json_value);
write_static_const(output, name, &serde_json::to_string_pretty(&camel_value).unwrap());
write_static_const(
output,
name,
&serde_json::to_string_pretty(&camel_value).unwrap(),
);
}
instance_const_camel(output, "TERM_NAMES", &TERM_NAMES);
@@ -336,13 +344,25 @@ fn camel_case_top_level_keys(value: Value) -> Value {
fn indent_json_const(json: &str) -> String {
json.lines()
.enumerate()
.map(|(i, line)| if i == 0 { line.to_string() } else { format!(" {}", line) })
.map(|(i, line)| {
if i == 0 {
line.to_string()
} else {
format!(" {}", line)
}
})
.collect::<Vec<_>>()
.join("\n")
}
fn write_static_const(output: &mut String, name: &str, json: &str) {
writeln!(output, " {} = /** @type {{const}} */ ({});\n", name, indent_json_const(json)).unwrap();
writeln!(
output,
" {} = /** @type {{const}} */ ({});\n",
name,
indent_json_const(json)
)
.unwrap();
}
/// Generate index accessor factory functions.
@@ -354,14 +374,30 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
writeln!(output, "// Index accessor factory functions\n").unwrap();
for pattern in patterns {
// Use 'readonly' to indicate these are getters (lazy evaluation)
let by_fields: Vec<String> = pattern
.indexes
.iter()
.map(|idx| format!("{}: MetricEndpointBuilder<T>", idx.serialize_long()))
.map(|idx| {
format!(
"readonly {}: MetricEndpointBuilder<T>",
idx.serialize_long()
)
})
.collect();
let by_type = format!("{{ {} }}", by_fields.join(", "));
writeln!(output, "/**").unwrap();
writeln!(
output,
" * Metric pattern with index endpoints as lazy getters."
)
.unwrap();
writeln!(
output,
" * Access via property (.by.dateindex) or bracket notation (.by['dateindex'])."
)
.unwrap();
writeln!(output, " * @template T").unwrap();
writeln!(
output,
@@ -385,7 +421,11 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
for (i, index) in pattern.indexes.iter().enumerate() {
let index_name = index.serialize_long();
let comma = if i < pattern.indexes.len() - 1 { "," } else { "" };
let comma = if i < pattern.indexes.len() - 1 {
","
} else {
""
};
writeln!(
output,
" get {}() {{ return _endpoint(client, name, '{}'); }}{}",
@@ -438,8 +478,12 @@ pub fn generate_structural_patterns(
}
writeln!(output, " * @typedef {{Object}} {}", pattern.name).unwrap();
for field in &pattern.fields {
let js_type =
metadata.field_type_annotation(field, pattern.is_generic, None, GenericSyntax::JAVASCRIPT);
let js_type = metadata.field_type_annotation(
field,
pattern.is_generic,
None,
GenericSyntax::JAVASCRIPT,
);
writeln!(
output,
" * @property {{{}}} {}",
@@ -469,8 +513,17 @@ pub fn generate_structural_patterns(
writeln!(output, " * @returns {{{}}}", return_type).unwrap();
writeln!(output, " */").unwrap();
let param_name = if is_parameterizable { "acc" } else { "basePath" };
writeln!(output, "function create{}(client, {}) {{", pattern.name, param_name).unwrap();
let param_name = if is_parameterizable {
"acc"
} else {
"basePath"
};
writeln!(
output,
"function create{}(client, {}) {{",
pattern.name, param_name
)
.unwrap();
writeln!(output, " return {{").unwrap();
let syntax = JavaScriptSyntax;

View File

@@ -6,7 +6,7 @@ use std::fmt::Write;
use brk_types::TreeNode;
use crate::{
ClientMetadata, Endpoint, GenericSyntax, JavaScriptSyntax, PatternField, child_type_name,
ClientMetadata, Endpoint, GenericSyntax, JavaScriptSyntax, PatternField,
generate_leaf_field, get_first_leaf_name, get_node_fields, get_pattern_instance_base,
infer_accumulated_name, prepare_tree_node, to_camel_case,
};
@@ -45,43 +45,41 @@ fn generate_tree_typedef(
writeln!(output, "/**").unwrap();
writeln!(output, " * @typedef {{Object}} {}", name).unwrap();
for ((field, child_fields), (child_name, _)) in
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
{
let js_type = metadata.resolve_tree_field_type(
field,
child_fields.as_deref(),
name,
child_name,
GenericSyntax::JAVASCRIPT,
);
for child in &ctx.children {
let js_type = if child.should_inline {
child.inline_type_name.clone()
} else {
metadata.resolve_tree_field_type(
&child.field,
child.child_fields.as_deref(),
name,
child.name,
GenericSyntax::JAVASCRIPT,
)
};
writeln!(
output,
" * @property {{{}}} {}",
js_type,
to_camel_case(&field.name)
to_camel_case(&child.field.name)
)
.unwrap();
}
writeln!(output, " */\n").unwrap();
for (child_name, child_node) in ctx.children {
if let TreeNode::Branch(grandchildren) = child_node {
let child_fields = get_node_fields(grandchildren, pattern_lookup);
// Generate typedef if no pattern match OR pattern is not parameterizable
if !metadata.is_parameterizable_fields(&child_fields) {
let child_type = child_type_name(name, child_name);
generate_tree_typedef(
output,
&child_type,
child_node,
pattern_lookup,
metadata,
generated,
);
}
// Generate child typedefs
for child in &ctx.children {
if child.should_inline {
generate_tree_typedef(
output,
&child.inline_type_name,
child.node,
pattern_lookup,
metadata,
generated,
);
}
}
}
@@ -182,12 +180,14 @@ fn generate_tree_initializer(
.get(&child_fields)
.filter(|name| metadata.is_parameterizable(name));
if let Some(pattern_name) = pattern_name {
let arg = get_pattern_instance_base(child_node);
let base_result = get_pattern_instance_base(child_node);
// Use pattern factory only if no outlier was detected
if let Some(pattern_name) = pattern_name.filter(|_| !base_result.has_outlier) {
writeln!(
output,
"{}{}: create{}(this, '{}'),",
indent_str, field_name, pattern_name, arg
indent_str, field_name, pattern_name, base_result.base
)
.unwrap();
} else {

View File

@@ -195,143 +195,138 @@ class _EndpointConfig:
class RangeBuilder(Generic[T]):
"""Final builder with range fully specified. Can only call json() or csv()."""
"""Builder with range specified."""
def __init__(self, config: _EndpointConfig):
self._config = config
def json(self) -> MetricData[T]:
"""Execute the query and return parsed JSON data."""
def fetch(self) -> MetricData[T]:
"""Fetch the range as parsed JSON."""
return self._config.get_json()
def csv(self) -> str:
"""Execute the query and return CSV data as a string."""
def fetch_csv(self) -> str:
"""Fetch the range as CSV string."""
return self._config.get_csv()
class FromBuilder(Generic[T]):
"""Builder after calling from(start). Can chain with take() or to()."""
class SingleItemBuilder(Generic[T]):
"""Builder for single item access."""
def __init__(self, config: _EndpointConfig):
self._config = config
def fetch(self) -> MetricData[T]:
"""Fetch the single item."""
return self._config.get_json()
def fetch_csv(self) -> str:
"""Fetch as CSV."""
return self._config.get_csv()
class SkippedBuilder(Generic[T]):
"""Builder after calling skip(n). Chain with take() to specify count."""
def __init__(self, config: _EndpointConfig):
self._config = config
def take(self, n: int) -> RangeBuilder[T]:
"""Take n items from the start position."""
"""Take n items after the skipped position."""
start = self._config.start or 0
return RangeBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
start, start + n
))
def to(self, end: int) -> RangeBuilder[T]:
"""Set the end position."""
return RangeBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
self._config.start, end
))
def json(self) -> MetricData[T]:
"""Execute the query and return parsed JSON data (from start to end of data)."""
def fetch(self) -> MetricData[T]:
"""Fetch from skipped position to end."""
return self._config.get_json()
def csv(self) -> str:
"""Execute the query and return CSV data as a string."""
return self._config.get_csv()
class ToBuilder(Generic[T]):
"""Builder after calling to(end). Can chain with take_last() or from()."""
def __init__(self, config: _EndpointConfig):
self._config = config
def take_last(self, n: int) -> RangeBuilder[T]:
"""Take last n items before the end position."""
end = self._config.end or 0
return RangeBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
end - n, end
))
def from_(self, start: int) -> RangeBuilder[T]:
"""Set the start position."""
return RangeBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
start, self._config.end
))
def json(self) -> MetricData[T]:
"""Execute the query and return parsed JSON data (from start of data to end)."""
return self._config.get_json()
def csv(self) -> str:
"""Execute the query and return CSV data as a string."""
def fetch_csv(self) -> str:
"""Fetch as CSV."""
return self._config.get_csv()
class MetricEndpointBuilder(Generic[T]):
"""Initial builder for metric endpoint queries.
"""Builder for metric endpoint queries.
Use method chaining to specify the data range, then call json() or csv() to execute.
Use method chaining to specify the data range, then call fetch() or fetch_csv() to execute.
Examples:
# Get all data
endpoint.json()
# Fetch all data
data = endpoint.fetch()
# Get last 10 points
endpoint.last(10).json()
# Single item access
data = endpoint[5].fetch()
# Get range [100, 200)
endpoint.range(100, 200).json()
# Slice syntax (Python-native)
data = endpoint[:10].fetch() # First 10
data = endpoint[-5:].fetch() # Last 5
data = endpoint[100:110].fetch() # Range
# Get 10 points starting from position 100
endpoint.from_(100).take(10).json()
# Convenience methods (pandas-style)
data = endpoint.head().fetch() # First 10 (default)
data = endpoint.head(20).fetch() # First 20
data = endpoint.tail(5).fetch() # Last 5
# Iterator-style chaining
data = endpoint.skip(100).take(10).fetch()
"""
def __init__(self, client: BrkClientBase, name: str, index: Index):
self._config = _EndpointConfig(client, name, index)
def first(self, n: int) -> RangeBuilder[T]:
"""Fetch the first n data points."""
@overload
def __getitem__(self, key: int) -> SingleItemBuilder[T]: ...
@overload
def __getitem__(self, key: slice) -> RangeBuilder[T]: ...
def __getitem__(self, key: Union[int, slice]) -> Union[SingleItemBuilder[T], RangeBuilder[T]]:
"""Access single item or slice.
Examples:
endpoint[5] # Single item at index 5
endpoint[:10] # First 10
endpoint[-5:] # Last 5
endpoint[100:110] # Range 100-109
"""
if isinstance(key, int):
return SingleItemBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
key, key + 1
))
return RangeBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
key.start, key.stop
))
def head(self, n: int = 10) -> RangeBuilder[T]:
"""Get the first n items (pandas-style)."""
return RangeBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
None, n
))
def last(self, n: int) -> RangeBuilder[T]:
"""Fetch the last n data points."""
def tail(self, n: int = 10) -> RangeBuilder[T]:
"""Get the last n items (pandas-style)."""
return RangeBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
-n, None
))
def range(self, start: int, end: int) -> RangeBuilder[T]:
"""Set an explicit range [start, end)."""
return RangeBuilder(_EndpointConfig(
def skip(self, n: int) -> SkippedBuilder[T]:
"""Skip the first n items. Chain with take() to get a range."""
return SkippedBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
start, end
n, None
))
def from_(self, start: int) -> FromBuilder[T]:
"""Set the start position. Chain with take() or to()."""
return FromBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
start, None
))
def to(self, end: int) -> ToBuilder[T]:
"""Set the end position. Chain with take_last() or from_()."""
return ToBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
None, end
))
def json(self) -> MetricData[T]:
"""Execute the query and return parsed JSON data (all data)."""
def fetch(self) -> MetricData[T]:
"""Fetch all data as parsed JSON."""
return self._config.get_json()
def csv(self) -> str:
"""Execute the query and return CSV data as a string (all data)."""
def fetch_csv(self) -> str:
"""Fetch all data as CSV string."""
return self._config.get_csv()
def path(self) -> str:

View File

@@ -26,7 +26,7 @@ pub fn generate_python_client(
writeln!(output, "# Do not edit manually\n").unwrap();
writeln!(
output,
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol"
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol, overload"
)
.unwrap();
writeln!(output, "from http.client import HTTPSConnection, HTTPConnection").unwrap();

View File

@@ -6,8 +6,8 @@ use std::fmt::Write;
use brk_types::TreeNode;
use crate::{
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, child_type_name, generate_leaf_field,
get_node_fields, get_pattern_instance_base, prepare_tree_node, to_snake_case,
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, generate_leaf_field,
prepare_tree_node, to_snake_case,
};
/// Generate tree classes
@@ -41,22 +41,16 @@ fn generate_tree_class(
// Generate child classes FIRST (post-order traversal)
// This ensures children are defined before parent references them
for (child_name, child_node) in ctx.children.iter() {
if let TreeNode::Branch(grandchildren) = child_node {
let child_fields = get_node_fields(grandchildren, pattern_lookup);
// Generate inline class if no pattern match OR pattern is not parameterizable
if !metadata.is_parameterizable_fields(&child_fields) {
let child_class = child_type_name(name, child_name);
generate_tree_class(
output,
&child_class,
child_node,
pattern_lookup,
metadata,
generated,
);
}
for child in &ctx.children {
if child.should_inline {
generate_tree_class(
output,
&child.inline_type_name,
child.node,
pattern_lookup,
metadata,
generated,
);
}
}
@@ -71,45 +65,36 @@ fn generate_tree_class(
.unwrap();
let syntax = PythonSyntax;
for ((field, child_fields_opt), (child_name, child_node)) in
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
{
let py_type = metadata.resolve_tree_field_type(
field,
child_fields_opt.as_deref(),
name,
child_name,
GenericSyntax::PYTHON,
);
let field_name_py = to_snake_case(&field.name);
for child in &ctx.children {
let field_name_py = to_snake_case(child.name);
if metadata.is_pattern_type(&field.rust_type) && metadata.is_parameterizable(&field.rust_type)
{
// Parameterizable pattern: use pattern class with metric base
let metric_base = get_pattern_instance_base(child_node);
writeln!(
output,
" self.{}: {} = {}(client, '{}')",
field_name_py, py_type, field.rust_type, metric_base
)
.unwrap();
} else if let TreeNode::Leaf(leaf) = child_node {
// Leaf node: use shared helper
generate_leaf_field(output, &syntax, "client", child_name, leaf, metadata, " ");
} else if field.is_branch() {
// Non-parameterizable pattern or regular branch: generate inline class
let inline_class = child_type_name(name, &field.name);
if child.is_leaf {
if let TreeNode::Leaf(leaf) = child.node {
generate_leaf_field(output, &syntax, "client", child.name, leaf, metadata, " ");
}
} else if child.should_inline {
// Inline class
writeln!(
output,
" self.{}: {} = {}(client)",
field_name_py, inline_class, inline_class
field_name_py, child.inline_type_name, child.inline_type_name
)
.unwrap();
} else {
panic!(
"Field '{}' has no matching index pattern. All metrics must be indexed.",
field.name
// Use pattern class with metric base
let py_type = metadata.resolve_tree_field_type(
&child.field,
child.child_fields.as_deref(),
name,
child.name,
GenericSyntax::PYTHON,
);
writeln!(
output,
" self.{}: {} = {}(client, '{}')",
field_name_py, py_type, child.field.rust_type, child.base_result.base
)
.unwrap();
}
}

View File

@@ -12,6 +12,7 @@ pub fn generate_imports(output: &mut String) {
writeln!(
output,
r#"use std::sync::Arc;
use std::ops::{{Bound, RangeBounds}};
use serde::de::DeserializeOwned;
pub use brk_cohort::*;
pub use brk_types::*;
@@ -193,21 +194,30 @@ impl EndpointConfig {{
/// Initial builder for metric endpoint queries.
///
/// Use method chaining to specify the data range, then call `json()` or `csv()` to execute.
/// Use method chaining to specify the data range, then call `fetch()` or `fetch_csv()` to execute.
///
/// # Examples
/// ```ignore
/// // Get all data
/// endpoint.json()?;
/// // Fetch all data
/// let data = endpoint.fetch()?;
///
/// // Get last 10 points
/// endpoint.last(10).json()?;
/// // Get single item at index 5
/// let data = endpoint.get(5).fetch()?;
///
/// // Get first 10 using range
/// let data = endpoint.range(..10).fetch()?;
///
/// // Get range [100, 200)
/// endpoint.range(100, 200).json()?;
/// let data = endpoint.range(100..200).fetch()?;
///
/// // Get 10 points starting from position 100
/// endpoint.from(100).take(10).json()?;
/// // Get first 10 (convenience)
/// let data = endpoint.take(10).fetch()?;
///
/// // Get last 10
/// let data = endpoint.last(10).fetch()?;
///
/// // Iterator-style chaining
/// let data = endpoint.skip(100).take(10).fetch()?;
/// ```
pub struct MetricEndpointBuilder<T> {{
config: EndpointConfig,
@@ -219,44 +229,59 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
Self {{ config: EndpointConfig::new(client, name, index), _marker: std::marker::PhantomData }}
}}
/// Fetch the first n data points.
pub fn first(mut self, n: u64) -> RangeBuilder<T> {{
self.config.end = Some(n as i64);
/// Select a specific index position.
pub fn get(mut self, index: usize) -> SingleItemBuilder<T> {{
self.config.start = Some(index as i64);
self.config.end = Some(index as i64 + 1);
SingleItemBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Select a range using Rust range syntax.
///
/// # Examples
/// ```ignore
/// endpoint.range(..10) // first 10
/// endpoint.range(100..110) // indices 100-109
/// endpoint.range(100..) // from 100 to end
/// ```
pub fn range<R: RangeBounds<usize>>(mut self, range: R) -> RangeBuilder<T> {{
self.config.start = match range.start_bound() {{
Bound::Included(&n) => Some(n as i64),
Bound::Excluded(&n) => Some(n as i64 + 1),
Bound::Unbounded => None,
}};
self.config.end = match range.end_bound() {{
Bound::Included(&n) => Some(n as i64 + 1),
Bound::Excluded(&n) => Some(n as i64),
Bound::Unbounded => None,
}};
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Fetch the last n data points.
pub fn last(mut self, n: u64) -> RangeBuilder<T> {{
/// Take the first n items.
pub fn take(self, n: usize) -> RangeBuilder<T> {{
self.range(..n)
}}
/// Take the last n items.
pub fn last(mut self, n: usize) -> RangeBuilder<T> {{
self.config.start = Some(-(n as i64));
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Set an explicit range [start, end).
pub fn range(mut self, start: i64, end: i64) -> RangeBuilder<T> {{
self.config.start = Some(start);
self.config.end = Some(end);
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
/// Skip the first n items. Chain with `take(n)` to get a range.
pub fn skip(mut self, n: usize) -> SkippedBuilder<T> {{
self.config.start = Some(n as i64);
SkippedBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Set the start position. Chain with `take(n)` or `to(end)`.
pub fn from(mut self, start: i64) -> FromBuilder<T> {{
self.config.start = Some(start);
FromBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Set the end position. Chain with `takeLast(n)` or `from(start)`.
pub fn to(mut self, end: i64) -> ToBuilder<T> {{
self.config.end = Some(end);
ToBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Execute the query and return parsed JSON data (all data).
pub fn json(self) -> Result<MetricData<T>> {{
/// Fetch all data as parsed JSON.
pub fn fetch(self) -> Result<MetricData<T>> {{
self.config.get_json(None)
}}
/// Execute the query and return CSV data as a string (all data).
pub fn csv(self) -> Result<String> {{
/// Fetch all data as CSV string.
pub fn fetch_csv(self) -> Result<String> {{
self.config.get_text(Some("csv"))
}}
@@ -266,82 +291,63 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
}}
}}
/// Builder after calling `from(start)`. Can chain with `take(n)` or `to(end)`.
pub struct FromBuilder<T> {{
/// Builder for single item access.
pub struct SingleItemBuilder<T> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
}}
impl<T: DeserializeOwned> FromBuilder<T> {{
/// Take n items from the start position.
pub fn take(mut self, n: u64) -> RangeBuilder<T> {{
impl<T: DeserializeOwned> SingleItemBuilder<T> {{
/// Fetch the single item.
pub fn fetch(self) -> Result<MetricData<T>> {{
self.config.get_json(None)
}}
/// Fetch the single item as CSV.
pub fn fetch_csv(self) -> Result<String> {{
self.config.get_text(Some("csv"))
}}
}}
/// Builder after calling `skip(n)`. Chain with `take(n)` to specify count.
pub struct SkippedBuilder<T> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
}}
impl<T: DeserializeOwned> SkippedBuilder<T> {{
/// Take n items after the skipped position.
pub fn take(mut self, n: usize) -> RangeBuilder<T> {{
let start = self.config.start.unwrap_or(0);
self.config.end = Some(start + n as i64);
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Set the end position.
pub fn to(mut self, end: i64) -> RangeBuilder<T> {{
self.config.end = Some(end);
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Execute the query and return parsed JSON data (from start to end of data).
pub fn json(self) -> Result<MetricData<T>> {{
/// Fetch from the skipped position to the end.
pub fn fetch(self) -> Result<MetricData<T>> {{
self.config.get_json(None)
}}
/// Execute the query and return CSV data as a string.
pub fn csv(self) -> Result<String> {{
/// Fetch from the skipped position to the end as CSV.
pub fn fetch_csv(self) -> Result<String> {{
self.config.get_text(Some("csv"))
}}
}}
/// Builder after calling `to(end)`. Can chain with `takeLast(n)` or `from(start)`.
pub struct ToBuilder<T> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
}}
impl<T: DeserializeOwned> ToBuilder<T> {{
/// Take last n items before the end position.
pub fn take_last(mut self, n: u64) -> RangeBuilder<T> {{
let end = self.config.end.unwrap_or(0);
self.config.start = Some(end - n as i64);
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Set the start position.
pub fn from(mut self, start: i64) -> RangeBuilder<T> {{
self.config.start = Some(start);
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Execute the query and return parsed JSON data (from start of data to end).
pub fn json(self) -> Result<MetricData<T>> {{
self.config.get_json(None)
}}
/// Execute the query and return CSV data as a string.
pub fn csv(self) -> Result<String> {{
self.config.get_text(Some("csv"))
}}
}}
/// Final builder with range fully specified. Can only call `json()` or `csv()`.
/// Builder with range fully specified.
pub struct RangeBuilder<T> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
}}
impl<T: DeserializeOwned> RangeBuilder<T> {{
/// Execute the query and return parsed JSON data.
pub fn json(self) -> Result<MetricData<T>> {{
/// Fetch the range as parsed JSON.
pub fn fetch(self) -> Result<MetricData<T>> {{
self.config.get_json(None)
}}
/// Execute the query and return CSV data as a string.
pub fn csv(self) -> Result<String> {{
/// Fetch the range as CSV string.
pub fn fetch_csv(self) -> Result<String> {{
self.config.get_text(Some("csv"))
}}
}}

View File

@@ -6,9 +6,8 @@ use std::fmt::Write;
use brk_types::TreeNode;
use crate::{
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax, child_type_name,
generate_leaf_field, generate_tree_node_field, get_node_fields, get_pattern_instance_base,
prepare_tree_node, to_snake_case,
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax,
generate_leaf_field, generate_tree_node_field, prepare_tree_node, to_snake_case,
};
/// Generate tree structs.
@@ -39,25 +38,29 @@ fn generate_tree_node(
return;
};
// Generate struct definition
writeln!(output, "/// Metrics tree node.").unwrap();
writeln!(output, "pub struct {} {{", name).unwrap();
for ((field, child_fields), (child_name, _)) in
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
{
let field_name = to_snake_case(&field.name);
let type_annotation = metadata.resolve_tree_field_type(
field,
child_fields.as_deref(),
name,
child_name,
GenericSyntax::RUST,
);
for child in &ctx.children {
let field_name = to_snake_case(child.name);
let type_annotation = if child.should_inline {
child.inline_type_name.clone()
} else {
metadata.resolve_tree_field_type(
&child.field,
child.child_fields.as_deref(),
name,
child.name,
GenericSyntax::RUST,
)
};
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
}
writeln!(output, "}}\n").unwrap();
// Generate impl block
writeln!(output, "impl {} {{", name).unwrap();
writeln!(
output,
@@ -67,53 +70,40 @@ fn generate_tree_node(
writeln!(output, " Self {{").unwrap();
let syntax = RustSyntax;
for ((field_info, child_fields), (child_name, child_node)) in
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
{
let field_name = to_snake_case(&field_info.name);
for child in &ctx.children {
let field_name = to_snake_case(child.name);
// Check if this is a pattern type and if it's parameterizable
let is_parameterizable = child_fields
.as_ref()
.is_some_and(|cf| metadata.is_parameterizable_fields(cf));
if metadata.is_pattern_type(&field_info.rust_type) && is_parameterizable {
// Parameterizable pattern: use pattern constructor with metric base
let pattern_base = get_pattern_instance_base(child_node);
generate_tree_node_field(
output,
&syntax,
field_info,
metadata,
" ",
child_name,
Some(&pattern_base),
);
} else if child_fields.is_some() {
// Non-parameterizable pattern or regular branch: use inline struct
let child_struct = child_type_name(name, child_name);
let path_expr = syntax.path_expr("base_path", &format!("_{}", child_name));
if child.is_leaf {
if let TreeNode::Leaf(leaf) = child.node {
generate_leaf_field(
output,
&syntax,
"client.clone()",
child.name,
leaf,
metadata,
" ",
);
}
} else if child.should_inline {
// Inline struct
let path_expr = syntax.path_expr("base_path", &format!("_{}", child.name));
writeln!(
output,
" {}: {}::new(client.clone(), {}),",
field_name, child_struct, path_expr
field_name, child.inline_type_name, path_expr
)
.unwrap();
} else if let TreeNode::Leaf(leaf) = child_node {
// Leaf field - use shared helper
generate_leaf_field(
} else {
// Use pattern constructor
generate_tree_node_field(
output,
&syntax,
"client.clone()",
child_name,
leaf,
&child.field,
metadata,
" ",
);
} else {
panic!(
"Field '{}' is a leaf with no TreeNode::Leaf. This shouldn't happen.",
field_info.name
child.name,
Some(&child.base_result.base),
);
}
}
@@ -122,21 +112,17 @@ fn generate_tree_node(
writeln!(output, " }}").unwrap();
writeln!(output, "}}\n").unwrap();
for (child_name, child_node) in ctx.children {
if let TreeNode::Branch(grandchildren) = child_node {
let child_fields = get_node_fields(grandchildren, pattern_lookup);
// Generate child struct if no pattern match OR pattern is not parameterizable
if !metadata.is_parameterizable_fields(&child_fields) {
let child_struct = child_type_name(name, child_name);
generate_tree_node(
output,
&child_struct,
child_node,
pattern_lookup,
metadata,
generated,
);
}
// Generate child structs
for child in &ctx.children {
if child.should_inline {
generate_tree_node(
output,
&child.inline_type_name,
child.node,
pattern_lookup,
metadata,
generated,
);
}
}
}

View File

@@ -23,7 +23,7 @@ brk_rpc = { workspace = true }
brk_server = { workspace = true }
clap = { version = "4.5.54", features = ["derive", "string"] }
color-eyre = { workspace = true }
importmap = "0.1.2"
importmap = "0.1.3"
# importmap = { path = "../../../importmap" }
tracing = { workspace = true }
minreq = { workspace = true }

View File

@@ -17,6 +17,7 @@ use brk_mempool::Mempool;
use brk_query::AsyncQuery;
use brk_reader::Reader;
use brk_server::{Server, VERSION};
use importmap::ImportMap;
use tracing::info;
use vecdb::Exit;
@@ -85,25 +86,28 @@ pub fn run() -> color_eyre::Result<()> {
let data_path = config.brkdir();
let future = async move {
let bundle_path = if website.is_some() {
// Try to find local dev directories - check cwd and parent directories
let find_dev_dirs = || -> Option<(PathBuf, PathBuf)> {
let mut dir = std::env::current_dir().ok()?;
loop {
let websites = dir.join("websites");
let modules = dir.join("modules");
if websites.exists() && modules.exists() {
return Some((websites, modules));
}
// Stop at workspace root (crates/ indicates we're there)
if dir.join("crates").exists() {
return None;
}
dir = dir.parent()?.to_path_buf();
// Try to find local dev directories - check cwd and parent directories
let find_dev_dirs = || -> Option<(PathBuf, PathBuf)> {
let mut dir = std::env::current_dir().ok()?;
loop {
let websites = dir.join("websites");
let modules = dir.join("modules");
if websites.exists() && modules.exists() {
return Some((websites, modules));
}
};
// Stop at workspace root (crates/ indicates we're there)
if dir.join("crates").exists() {
return None;
}
dir = dir.parent()?.to_path_buf();
}
};
let websites_path = if let Some((websites, _modules)) = find_dev_dirs() {
let dev_dirs = find_dev_dirs();
let is_dev = dev_dirs.is_some();
let bundle_path = if website.is_some() {
let websites_path = if let Some((websites, _modules)) = dev_dirs {
websites
} else {
let downloaded_brk_path = downloads_path.join(format!("brk-{VERSION}"));
@@ -133,19 +137,28 @@ pub fn run() -> color_eyre::Result<()> {
None
};
// Generate import map for cache busting
// Generate import map for cache busting (disabled in dev mode)
if let Some(ref path) = bundle_path {
match importmap::ImportMap::scan(path, "") {
Ok(map) => {
let html_path = path.join("index.html");
if let Ok(html) = fs::read_to_string(&html_path)
&& let Some(updated) = map.update_html(&html)
{
let _ = fs::write(&html_path, updated);
info!("Updated importmap in index.html");
let map = if is_dev {
ImportMap::empty()
} else {
match ImportMap::scan(path, "") {
Ok(map) => map,
Err(e) => {
tracing::error!("Failed to generate importmap: {e}");
ImportMap::empty()
}
}
Err(e) => tracing::error!("Failed to generate importmap: {e}"),
};
let html_path = path.join("index.html");
if let Ok(html) = fs::read_to_string(&html_path)
&& let Some(updated) = map.update_html(&html)
{
let _ = fs::write(&html_path, updated);
if !is_dev {
info!("Updated importmap in index.html");
}
}
}

View File

@@ -14,6 +14,7 @@ fn main() -> brk_client::Result<()> {
});
// Fetch price data using the typed metrics API
// Using new idiomatic API: last(3).fetch()
let price_close = client
.metrics()
.price
@@ -22,8 +23,8 @@ fn main() -> brk_client::Result<()> {
.close
.by
.dateindex()
.from(-3)
.json()?;
.last(3)
.fetch()?;
println!("Last 3 price close values: {:?}", price_close);
// Fetch block data
@@ -35,12 +36,11 @@ fn main() -> brk_client::Result<()> {
.sum
.by
.dateindex()
.from(-3)
.json()?;
.last(3)
.fetch()?;
println!("Last 3 block count values: {:?}", block_count);
// Fetch supply data
//
dbg!(
client
.metrics()
@@ -58,8 +58,8 @@ fn main() -> brk_client::Result<()> {
.bitcoin
.by
.dateindex()
.from(-3)
.csv()?;
.last(3)
.fetch_csv()?;
println!("Last 3 circulating supply values: {:?}", circulating);
// Using generic metric fetching

File diff suppressed because it is too large Load Diff

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_error::Result;
use brk_fetcher::Fetcher;
use brk_indexer::Indexer;
use vecdb::{AnyStoredVec, Exit};
use vecdb::{AnySerializableVec, AnyVec, Exit};
pub fn main() -> Result<()> {
// Can't increase main thread's stack size, thus we need to use another thread
@@ -19,7 +19,6 @@ fn run() -> Result<()> {
brk_logger::init(None)?;
let outputs_dir = Path::new(&env::var("HOME").unwrap()).join(".brk");
// let outputs_dir = Path::new("../../_outputs");
let indexer = Indexer::forced_import(&outputs_dir)?;
@@ -30,7 +29,35 @@ fn run() -> Result<()> {
let computer = Computer::forced_import(&outputs_dir, &indexer, Some(fetcher))?;
let _a = dbg!(computer.transactions.fees.fee.base.region().meta());
// Test emptyaddressdata (underlying BytesVec) - direct access
let empty_data = &computer.distribution.addresses_data.empty;
println!("emptyaddressdata (BytesVec) len: {}", empty_data.len());
let start = Instant::now();
let mut buf = Vec::new();
empty_data.write_json(Some(empty_data.len() - 1), Some(empty_data.len()), &mut buf)?;
println!("emptyaddressdata last item JSON: {}", String::from_utf8_lossy(&buf));
println!("Time for BytesVec write_json: {:?}", start.elapsed());
// Test emptyaddressindex (LazyVecFrom1 wrapper) - computed access
let empty_index = &computer.distribution.emptyaddressindex;
println!("\nemptyaddressindex (LazyVecFrom1) len: {}", empty_index.len());
let start = Instant::now();
let mut buf = Vec::new();
empty_index.write_json(Some(empty_index.len() - 1), Some(empty_index.len()), &mut buf)?;
println!("emptyaddressindex last item JSON: {}", String::from_utf8_lossy(&buf));
println!("Time for LazyVecFrom1 write_json: {:?}", start.elapsed());
// Compare with loaded versions
let loaded_data = &computer.distribution.addresses_data.loaded;
println!("\nloadedaddressdata (BytesVec) len: {}", loaded_data.len());
let start = Instant::now();
let mut buf = Vec::new();
loaded_data.write_json(Some(loaded_data.len() - 1), Some(loaded_data.len()), &mut buf)?;
println!("loadedaddressdata last item JSON: {}", String::from_utf8_lossy(&buf));
println!("Time for BytesVec write_json: {:?}", start.elapsed());
Ok(())
}

View File

@@ -137,6 +137,18 @@ pub enum Error {
impl Error {
/// Returns true if this error is due to a file lock (another process has the database open).
/// Lock errors are transient and should not trigger data deletion.
pub fn is_lock_error(&self) -> bool {
matches!(self, Error::VecDB(e) if e.is_lock_error())
}
/// Returns true if this error indicates data corruption or version incompatibility.
/// These errors may require resetting/deleting the data to recover.
pub fn is_data_error(&self) -> bool {
matches!(self, Error::VecDB(e) if e.is_data_error())
}
/// Returns true if this network/fetch error indicates a permanent/blocking condition
/// that won't be resolved by retrying (e.g., DNS failure, connection refused, blocked endpoint).
/// Returns false for transient errors worth retrying (timeouts, rate limits, server errors).

View File

@@ -47,27 +47,26 @@ impl Indexer {
let indexed_path = outputs_dir.join("indexed");
let try_import = || -> Result<Self> {
let (vecs, stores) = thread::scope(|s| -> Result<_> {
let vecs = s.spawn(|| -> Result<_> {
let i = Instant::now();
let vecs = Vecs::forced_import(&indexed_path, VERSION)?;
info!("Imported vecs in {:?}", i.elapsed());
Ok(vecs)
});
let i = Instant::now();
let vecs = Vecs::forced_import(&indexed_path, VERSION)?;
info!("Imported vecs in {:?}", i.elapsed());
let i = Instant::now();
let stores = Stores::forced_import(&indexed_path, VERSION)?;
info!("Imported stores in {:?}", i.elapsed());
Ok((vecs.join().unwrap()?, stores))
})?;
let i = Instant::now();
let stores = Stores::forced_import(&indexed_path, VERSION)?;
info!("Imported stores in {:?}", i.elapsed());
Ok(Self { vecs, stores })
};
match try_import() {
Ok(result) => Ok(result),
Err(err) if can_retry => {
Err(err) if err.is_lock_error() => {
// Lock errors are transient - another process has the database open.
// Don't delete data, just return the error.
Err(err)
}
Err(err) if can_retry && err.is_data_error() => {
// Data corruption or version mismatch - safe to delete and retry
info!("{err:?}, deleting {indexed_path:?} and retrying");
fs::remove_dir_all(&indexed_path)?;
Self::forced_import_inner(outputs_dir, false)

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,10 @@
"bugs": {
"url": "https://github.com/bitcoinresearchkit/brk/issues"
},
"scripts": {
"test": "node tests/basic.js",
"test:tree": "node tests/tree.js"
},
"description": "BRK JavaScript client",
"engines": {
"node": ">=18"

View File

@@ -1,5 +1,74 @@
import { BrkClient } from "../index.js";
let client = new BrkClient("http://localhost:3110");
const client = new BrkClient("http://localhost:3110");
let blocks = await client.getBlocks();
console.log("Testing idiomatic API...\n");
// Test getter access (property)
console.log("1. Getter access (.by.dateindex):");
const all = await client.metrics.price.usd.split.close.by.dateindex;
console.log(` Total: ${all.total}, Got: ${all.data.length} items\n`);
// Test dynamic access (bracket notation)
console.log("2. Dynamic access (.by['dateindex']):");
const allDynamic = await client.metrics.price.usd.split.close.by["dateindex"];
console.log(
` Total: ${allDynamic.total}, Got: ${allDynamic.data.length} items\n`,
);
// Test fetch all (explicit .fetch())
console.log("3. Explicit .fetch():");
const allExplicit =
await client.metrics.price.usd.split.close.by.dateindex.fetch();
console.log(
` Total: ${allExplicit.total}, Got: ${allExplicit.data.length} items\n`,
);
// Test first(n)
console.log("4. First 5 items (.first(5)):");
const first5 = await client.metrics.price.usd.split.close.by.dateindex.first(5);
console.log(
` Total: ${first5.total}, Start: ${first5.start}, End: ${first5.end}, Got: ${first5.data.length} items\n`,
);
// Test last(n)
console.log("5. Last 5 items (.last(5)):");
const last5 = await client.metrics.price.usd.split.close.by.dateindex.last(5);
console.log(
` Total: ${last5.total}, Start: ${last5.start}, End: ${last5.end}, Got: ${last5.data.length} items\n`,
);
// Test slice(start, end)
console.log("6. Slice 10-20 (.slice(10, 20)):");
const sliced = await client.metrics.price.usd.split.close.by.dateindex.slice(
10,
20,
);
console.log(
` Total: ${sliced.total}, Start: ${sliced.start}, End: ${sliced.end}, Got: ${sliced.data.length} items\n`,
);
// Test get(index) - single item
console.log("7. Single item (.get(100)):");
const single = await client.metrics.price.usd.split.close.by.dateindex.get(100);
console.log(
` Total: ${single.total}, Start: ${single.start}, End: ${single.end}, Got: ${single.data.length} item(s)\n`,
);
// Test skip(n).take(m) chaining
console.log("8. Skip and take (.skip(100).take(10)):");
const skipTake = await client.metrics.price.usd.split.close.by.dateindex
.skip(100)
.take(10);
console.log(
` Total: ${skipTake.total}, Start: ${skipTake.start}, End: ${skipTake.end}, Got: ${skipTake.data.length} items\n`,
);
// Test fetchCsv
console.log("9. Fetch as CSV (.last(3).fetchCsv()):");
const csv = await client.metrics.price.usd.split.close.by.dateindex
.last(3)
.fetchCsv();
console.log(` CSV preview: ${csv.substring(0, 100)}...\n`);
console.log("All tests passed!");

View File

@@ -6,9 +6,9 @@ import { BrkClient } from "../index.js";
/**
* Recursively collect all metric patterns from the tree.
* @param {object} obj
* @param {Record<string, any>} obj
* @param {string} path
* @returns {Array<{path: string, metric: object, indexes: string[]}>}
* @returns {Array<{path: string, metric: Record<string, any>, indexes: string[]}>}
*/
function getAllMetrics(obj, path = "") {
const metrics = [];
@@ -21,10 +21,10 @@ function getAllMetrics(obj, path = "") {
const currentPath = path ? `${path}.${key}` : key;
// Check if this is a metric pattern (has 'by' property with index methods)
// Check if this is a metric pattern (has 'by' property with index getters)
if (attr.by && typeof attr.by === "object") {
const indexes = Object.keys(attr.by).filter(
(k) => !k.startsWith("_") && typeof attr.by[k] === "function",
(k) => !k.startsWith("_") && typeof attr.by[k] === "object",
);
if (indexes.length > 0) {
metrics.push({ path: currentPath, metric: attr, indexes });
@@ -41,54 +41,38 @@ function getAllMetrics(obj, path = "") {
}
async function testAllEndpoints() {
const client = new BrkClient("http://localhost:3110");
const client = new BrkClient({ baseUrl: "http://localhost:3110", timeout: 15000 });
const metrics = getAllMetrics(client.tree);
const metrics = getAllMetrics(client.metrics);
console.log(`\nFound ${metrics.length} metrics`);
let success = 0;
let failed = 0;
const errors = [];
for (const { path, metric, indexes } of metrics) {
for (const idxName of indexes) {
try {
const endpoint = metric.by[idxName]();
const res = await endpoint.range(-3);
const endpoint = metric.by[idxName];
const res = await endpoint.last(1);
const count = res.data.length;
if (count !== 3) {
failed++;
const errorMsg = `FAIL: ${path}.by.${idxName}() -> expected 3, got ${count}`;
errors.push(errorMsg);
console.log(errorMsg);
} else {
success++;
console.log(`OK: ${path}.by.${idxName}() -> ${count} items`);
if (count !== 1) {
console.log(
`FAIL: ${path}.by.${idxName} -> expected 1, got ${count}`,
);
return;
}
success++;
console.log(`OK: ${path}.by.${idxName} -> ${count} items`);
} catch (e) {
failed++;
const errorMsg = `FAIL: ${path}.by.${idxName}() -> ${e.message}`;
errors.push(errorMsg);
console.log(errorMsg);
console.log(
`FAIL: ${path}.by.${idxName} -> ${e instanceof Error ? e.message : e}`,
);
return;
}
}
}
console.log(`\n=== Results ===`);
console.log(`Success: ${success}`);
console.log(`Failed: ${failed}`);
if (errors.length > 0) {
console.log(`\nErrors:`);
errors.slice(0, 10).forEach((err) => console.log(` ${err}`));
if (errors.length > 10) {
console.log(` ... and ${errors.length - 10} more`);
}
}
if (failed > 0) {
process.exit(1);
}
}
testAllEndpoints();

File diff suppressed because it is too large Load Diff

View File

@@ -36,29 +36,30 @@ def test_fetch_csv_metric():
def test_fetch_typed_metric():
client = BrkClient("http://localhost:3110")
a = client.metrics.constants.constant_0.by.dateindex().from_(-10).json()
# Using new idiomatic API: tail(10).fetch() or [-10:].fetch()
a = client.metrics.constants.constant_0.by.dateindex().tail(10).fetch()
print(a)
b = client.metrics.outputs.count.utxo_count.by.height().from_(-10).json()
b = client.metrics.outputs.count.utxo_count.by.height().tail(10).fetch()
print(b)
c = client.metrics.price.usd.split.close.by.dateindex().from_(-10).json()
c = client.metrics.price.usd.split.close.by.dateindex().tail(10).fetch()
print(c)
d = (
client.metrics.market.dca.period_lump_sum_stack._10y.dollars.by.dateindex()
.from_(-10)
.json()
.tail(10)
.fetch()
)
print(d)
e = (
client.metrics.market.dca.class_average_price._2017.by.dateindex()
.from_(-10)
.json()
.tail(10)
.fetch()
)
print(e)
f = (
client.metrics.distribution.address_cohorts.amount_range._10k_sats_to_100k_sats.activity.sent.dollars.cumulative.by.dateindex()
.from_(-10)
.json()
.tail(10)
.fetch()
)
print(f)
g = client.metrics.price.usd.ohlc.by.dateindex().from_(-10).json()
g = client.metrics.price.usd.ohlc.by.dateindex().tail(10).fetch()
print(g)

View File

@@ -41,7 +41,7 @@ def test_all_endpoints():
"""Test fetching last 3 values from all metric endpoints."""
client = BrkClient("http://localhost:3110")
metrics = get_all_metrics(client.tree)
metrics = get_all_metrics(client.metrics)
print(f"\nFound {len(metrics)} metrics")
success = 0
@@ -53,7 +53,8 @@ def test_all_endpoints():
try:
by = metric.by
endpoint = getattr(by, idx_name)()
res = endpoint.range(-3)
# Use the new idiomatic API: tail(3).fetch() or [-3:].fetch()
res = endpoint.tail(3).fetch()
count = len(res["data"])
if count != 3:
failed += 1

View File

@@ -1515,148 +1515,7 @@
</style>
<!-- IMPORTMAP -->
<script type="importmap">
{
"imports": {
"/scripts/chart/index.js": "/scripts/chart/index.024e5d6b.js",
"/scripts/chart/oklch.js": "/scripts/chart/oklch.21450255.js",
"/scripts/entry.js": "/scripts/entry.93446a1b.js",
"/scripts/lazy.js": "/scripts/lazy.1ae52534.js",
"/scripts/main.js": "/scripts/main.22a5bd79.js",
"/scripts/modules/brk-client/index.js": "/scripts/modules/brk-client/index.664c39c8.js",
"/scripts/modules/brk-client/tests/basic.js": "/scripts/modules/brk-client/tests/basic.b92ff866.js",
"/scripts/modules/brk-client/tests/tree.js": "/scripts/modules/brk-client/tests/tree.ba9474f7.js",
"/scripts/modules/lean-qr/2.6.1/index.mjs": "/scripts/modules/lean-qr/2.6.1/index.09195c13.mjs",
"/scripts/modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs": "/scripts/modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.803b7fb0.mjs",
"/scripts/modules/lightweight-charts/5.0.9/dist/lightweight-charts.standalone.production.mjs": "/scripts/modules/lightweight-charts/5.0.9/dist/lightweight-charts.standalone.production.1e264451.mjs",
"/scripts/modules/lightweight-charts/5.1.0/dist/lightweight-charts.standalone.production.mjs": "/scripts/modules/lightweight-charts/5.1.0/dist/lightweight-charts.standalone.production.5c2a821a.mjs",
"/scripts/modules/modern-screenshot/4.6.6/dist/index.mjs": "/scripts/modules/modern-screenshot/4.6.6/dist/index.0f951334.mjs",
"/scripts/modules/modern-screenshot/4.6.7/dist/index.js": "/scripts/modules/modern-screenshot/4.6.7/dist/index.070574d6.js",
"/scripts/modules/modern-screenshot/4.6.7/dist/index.mjs": "/scripts/modules/modern-screenshot/4.6.7/dist/index.e9e389fe.mjs",
"/scripts/modules/modern-screenshot/4.6.7/dist/worker.js": "/scripts/modules/modern-screenshot/4.6.7/dist/worker.1265d9cd.js",
"/scripts/modules/solidjs-signals/0.6.3/dist/prod.js": "/scripts/modules/solidjs-signals/0.6.3/dist/prod.2f80e335.js",
"/scripts/modules/solidjs-signals/0.8.5/dist/prod.js": "/scripts/modules/solidjs-signals/0.8.5/dist/prod.8ae56250.js",
"/scripts/options/_partial_old.js": "/scripts/options/_partial_old.62bf3faa.js",
"/scripts/options/chain.js": "/scripts/options/chain.c173ace8.js",
"/scripts/options/cohorts/address.js": "/scripts/options/cohorts/address.2e8a42cb.js",
"/scripts/options/cohorts/data.js": "/scripts/options/cohorts/data.11381d83.js",
"/scripts/options/cohorts/index.js": "/scripts/options/cohorts/index.b0e57c9d.js",
"/scripts/options/cohorts/shared.js": "/scripts/options/cohorts/shared.87b9837c.js",
"/scripts/options/cohorts/utxo.js": "/scripts/options/cohorts/utxo.f0e69857.js",
"/scripts/options/cointime.js": "/scripts/options/cointime.e25633d9.js",
"/scripts/options/colors/cohorts.js": "/scripts/options/colors/cohorts.262d4551.js",
"/scripts/options/colors/index.js": "/scripts/options/colors/index.a54dc83f.js",
"/scripts/options/colors/misc.js": "/scripts/options/colors/misc.bee7dbee.js",
"/scripts/options/constants.js": "/scripts/options/constants.16dfce27.js",
"/scripts/options/context.js": "/scripts/options/context.8bf2932e.js",
"/scripts/options/full.js": "/scripts/options/full.11772605.js",
"/scripts/options/market/averages.js": "/scripts/options/market/averages.d95aa3e1.js",
"/scripts/options/market/index.js": "/scripts/options/market/index.e3b750d6.js",
"/scripts/options/market/indicators/bands.js": "/scripts/options/market/indicators/bands.81b19b83.js",
"/scripts/options/market/indicators/index.js": "/scripts/options/market/indicators/index.70e9b3e4.js",
"/scripts/options/market/indicators/momentum.js": "/scripts/options/market/indicators/momentum.48e71442.js",
"/scripts/options/market/indicators/onchain.js": "/scripts/options/market/indicators/onchain.f75ddfd9.js",
"/scripts/options/market/indicators/volatility.js": "/scripts/options/market/indicators/volatility.08ec92d7.js",
"/scripts/options/market/investing.js": "/scripts/options/market/investing.57ded805.js",
"/scripts/options/market/performance.js": "/scripts/options/market/performance.36c7ad40.js",
"/scripts/options/market/utils.js": "/scripts/options/market/utils.e3e058d8.js",
"/scripts/options/partial.js": "/scripts/options/partial.ab8fdf12.js",
"/scripts/options/series.js": "/scripts/options/series.5a2a34ed.js",
"/scripts/options/types.js": "/scripts/options/types.64db5149.js",
"/scripts/options/unused.js": "/scripts/options/unused.24a71427.js",
"/scripts/panes/chart/index.js": "/scripts/panes/chart/index.947ceee8.js",
"/scripts/panes/chart/screenshot.js": "/scripts/panes/chart/screenshot.adc8da89.js",
"/scripts/panes/explorer.js": "/scripts/panes/explorer.91a5a9ae.js",
"/scripts/panes/nav.js": "/scripts/panes/nav.0338dc4b.js",
"/scripts/panes/search.js": "/scripts/panes/search.0338dc4b.js",
"/scripts/panes/simulation.js": "/scripts/panes/simulation.abf9ee5d.js",
"/scripts/panes/table.js": "/scripts/panes/table.00486691.js",
"/scripts/resources.js": "/scripts/resources.77bdf76f.js",
"/scripts/signals.js": "/scripts/signals.2ba0669e.js",
"/scripts/utils/array.js": "/scripts/utils/array.1863f57c.js",
"/scripts/utils/colors.js": "/scripts/utils/colors.c95b2e03.js",
"/scripts/utils/date.js": "/scripts/utils/date.12ad717d.js",
"/scripts/utils/dom.js": "/scripts/utils/dom.4d99f37f.js",
"/scripts/utils/elements.js": "/scripts/utils/elements.6fe024ed.js",
"/scripts/utils/env.js": "/scripts/utils/env.594127e7.js",
"/scripts/utils/format.js": "/scripts/utils/format.4bdbfe40.js",
"/scripts/utils/serde.js": "/scripts/utils/serde.f9c7ed2b.js",
"/scripts/utils/storage.js": "/scripts/utils/storage.cbd2ff9c.js",
"/scripts/utils/timing.js": "/scripts/utils/timing.ae6a47b8.js",
"/scripts/utils/units.js": "/scripts/utils/units.30278ea7.js",
"/scripts/utils/url.js": "/scripts/utils/url.20469bf9.js",
"/scripts/utils/ws.js": "/scripts/utils/ws.fe3fb4b1.js"
}
}
</script>
<link rel="modulepreload" href="/scripts/chart/index.024e5d6b.js">
<link rel="modulepreload" href="/scripts/chart/oklch.21450255.js">
<link rel="modulepreload" href="/scripts/entry.93446a1b.js">
<link rel="modulepreload" href="/scripts/lazy.1ae52534.js">
<link rel="modulepreload" href="/scripts/main.22a5bd79.js">
<link rel="modulepreload" href="/scripts/modules/brk-client/index.664c39c8.js">
<link rel="modulepreload" href="/scripts/modules/brk-client/tests/basic.b92ff866.js">
<link rel="modulepreload" href="/scripts/modules/brk-client/tests/tree.ba9474f7.js">
<link rel="modulepreload" href="/scripts/modules/lean-qr/2.6.1/index.09195c13.mjs">
<link rel="modulepreload" href="/scripts/modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.803b7fb0.mjs">
<link rel="modulepreload" href="/scripts/modules/lightweight-charts/5.0.9/dist/lightweight-charts.standalone.production.1e264451.mjs">
<link rel="modulepreload" href="/scripts/modules/lightweight-charts/5.1.0/dist/lightweight-charts.standalone.production.5c2a821a.mjs">
<link rel="modulepreload" href="/scripts/modules/modern-screenshot/4.6.6/dist/index.0f951334.mjs">
<link rel="modulepreload" href="/scripts/modules/modern-screenshot/4.6.7/dist/index.070574d6.js">
<link rel="modulepreload" href="/scripts/modules/modern-screenshot/4.6.7/dist/index.e9e389fe.mjs">
<link rel="modulepreload" href="/scripts/modules/modern-screenshot/4.6.7/dist/worker.1265d9cd.js">
<link rel="modulepreload" href="/scripts/modules/solidjs-signals/0.6.3/dist/prod.2f80e335.js">
<link rel="modulepreload" href="/scripts/modules/solidjs-signals/0.8.5/dist/prod.8ae56250.js">
<link rel="modulepreload" href="/scripts/options/_partial_old.62bf3faa.js">
<link rel="modulepreload" href="/scripts/options/chain.c173ace8.js">
<link rel="modulepreload" href="/scripts/options/cohorts/address.2e8a42cb.js">
<link rel="modulepreload" href="/scripts/options/cohorts/data.11381d83.js">
<link rel="modulepreload" href="/scripts/options/cohorts/index.b0e57c9d.js">
<link rel="modulepreload" href="/scripts/options/cohorts/shared.87b9837c.js">
<link rel="modulepreload" href="/scripts/options/cohorts/utxo.f0e69857.js">
<link rel="modulepreload" href="/scripts/options/cointime.e25633d9.js">
<link rel="modulepreload" href="/scripts/options/colors/cohorts.262d4551.js">
<link rel="modulepreload" href="/scripts/options/colors/index.a54dc83f.js">
<link rel="modulepreload" href="/scripts/options/colors/misc.bee7dbee.js">
<link rel="modulepreload" href="/scripts/options/constants.16dfce27.js">
<link rel="modulepreload" href="/scripts/options/context.8bf2932e.js">
<link rel="modulepreload" href="/scripts/options/full.11772605.js">
<link rel="modulepreload" href="/scripts/options/market/averages.d95aa3e1.js">
<link rel="modulepreload" href="/scripts/options/market/index.e3b750d6.js">
<link rel="modulepreload" href="/scripts/options/market/indicators/bands.81b19b83.js">
<link rel="modulepreload" href="/scripts/options/market/indicators/index.70e9b3e4.js">
<link rel="modulepreload" href="/scripts/options/market/indicators/momentum.48e71442.js">
<link rel="modulepreload" href="/scripts/options/market/indicators/onchain.f75ddfd9.js">
<link rel="modulepreload" href="/scripts/options/market/indicators/volatility.08ec92d7.js">
<link rel="modulepreload" href="/scripts/options/market/investing.57ded805.js">
<link rel="modulepreload" href="/scripts/options/market/performance.36c7ad40.js">
<link rel="modulepreload" href="/scripts/options/market/utils.e3e058d8.js">
<link rel="modulepreload" href="/scripts/options/partial.ab8fdf12.js">
<link rel="modulepreload" href="/scripts/options/series.5a2a34ed.js">
<link rel="modulepreload" href="/scripts/options/types.64db5149.js">
<link rel="modulepreload" href="/scripts/options/unused.24a71427.js">
<link rel="modulepreload" href="/scripts/panes/chart/index.947ceee8.js">
<link rel="modulepreload" href="/scripts/panes/chart/screenshot.adc8da89.js">
<link rel="modulepreload" href="/scripts/panes/explorer.91a5a9ae.js">
<link rel="modulepreload" href="/scripts/panes/nav.0338dc4b.js">
<link rel="modulepreload" href="/scripts/panes/search.0338dc4b.js">
<link rel="modulepreload" href="/scripts/panes/simulation.abf9ee5d.js">
<link rel="modulepreload" href="/scripts/panes/table.00486691.js">
<link rel="modulepreload" href="/scripts/resources.77bdf76f.js">
<link rel="modulepreload" href="/scripts/signals.2ba0669e.js">
<link rel="modulepreload" href="/scripts/utils/array.1863f57c.js">
<link rel="modulepreload" href="/scripts/utils/colors.c95b2e03.js">
<link rel="modulepreload" href="/scripts/utils/date.12ad717d.js">
<link rel="modulepreload" href="/scripts/utils/dom.4d99f37f.js">
<link rel="modulepreload" href="/scripts/utils/elements.6fe024ed.js">
<link rel="modulepreload" href="/scripts/utils/env.594127e7.js">
<link rel="modulepreload" href="/scripts/utils/format.4bdbfe40.js">
<link rel="modulepreload" href="/scripts/utils/serde.f9c7ed2b.js">
<link rel="modulepreload" href="/scripts/utils/storage.cbd2ff9c.js">
<link rel="modulepreload" href="/scripts/utils/timing.ae6a47b8.js">
<link rel="modulepreload" href="/scripts/utils/units.30278ea7.js">
<link rel="modulepreload" href="/scripts/utils/url.20469bf9.js">
<link rel="modulepreload" href="/scripts/utils/ws.fe3fb4b1.js">
<!-- /IMPORTMAP -->
<!-- ------- -->

View File

@@ -19,6 +19,7 @@ import { throttle } from "../utils/timing.js";
import { serdeBool } from "../utils/serde.js";
import { stringToId } from "../utils/format.js";
import { style } from "../utils/elements.js";
import { resources } from "../resources.js";
/**
* @typedef {Object} Valued
@@ -70,20 +71,18 @@ const lineWidth = /** @type {any} */ (1.5);
* @param {HTMLElement} args.parent
* @param {Signals} args.signals
* @param {Colors} args.colors
* @param {Resources} args.resources
* @param {BrkClient} args.brk
* @param {Accessor<ChartableIndex>} args.index
* @param {((unknownTimeScaleCallback: VoidFunction) => void)} [args.timeScaleSetCallback]
* @param {true} [args.fitContent]
* @param {{unit: Unit; blueprints: AnySeriesBlueprint[]}[]} [args.config]
*/
function createChartElement({
export function createChartElement({
parent,
signals,
colors,
id: chartId,
index,
resources,
brk,
timeScaleSetCallback,
fitContent,
@@ -1076,5 +1075,3 @@ function numberToUSFormat(value, digits, options) {
* @typedef {typeof createChartElement} CreateChartElement
* @typedef {ReturnType<createChartElement>} Chart
*/
export default { createChartElement };

View File

@@ -6,7 +6,7 @@
* @import { Signal, Signals, Accessor } from "./signals.js";
*
* @import * as Brk from "./modules/brk-client/index.js"
* @import { BrkClient} from "./modules/brk-client/index.js"
* @import { BrkClient, Index, Metric, MetricData } from "./modules/brk-client/index.js"
*
* @import { Resources, MetricResource } from './resources.js'
*
@@ -28,23 +28,91 @@
// import uFuzzy = require("./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.d.ts");
/**
* @typedef {typeof import("./lazy")["default"]} Modules
* @typedef {[number, number, number, number]} OHLCTuple
*
* Brk type aliases
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts} UtxoCohortTree
* @typedef {Brk.MetricsTree_Distribution_AddressCohorts} AddressCohortTree
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_All} AllUtxoPattern
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_Term_Short} ShortTermPattern
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_Term_Long} LongTermPattern
* @typedef {Brk._10yPattern} MaxAgePattern
* @typedef {Brk._10yTo12yPattern} AgeRangePattern
* @typedef {Brk._0satsPattern2} UtxoAmountPattern
* @typedef {Brk._0satsPattern} AddressAmountPattern
* @typedef {Brk._100btcPattern} BasicUtxoPattern
* @typedef {Brk._0satsPattern2} EpochPattern
* @typedef {Brk.Ratio1ySdPattern} Ratio1ySdPattern
* @typedef {Brk.Dollars} Dollars
* @typedef {Brk.Price111dSmaPattern} EmaRatioPattern
* @typedef {Brk.CoinbasePattern} CoinbasePattern
* @typedef {Brk.ActivePriceRatioPattern} ActivePriceRatioPattern
* @typedef {Brk.UnclaimedRewardsPattern} ValuePattern
* @typedef {Brk.AnyMetricPattern} AnyMetricPattern
* @typedef {Brk.AnyMetricEndpointBuilder} AnyMetricEndpoint
* @typedef {Brk.AnyMetricData} AnyMetricData
* @typedef {Brk.AddrCountPattern} AddrCountPattern
* @typedef {Brk.MetricsTree_Blocks_Interval} IntervalPattern
* @typedef {Brk.MetricsTree_Supply_Circulating} SupplyPattern
* @typedef {Brk.RelativePattern} GlobalRelativePattern
* @typedef {Brk.RelativePattern2} OwnRelativePattern
* @typedef {Brk.RelativePattern5} FullRelativePattern
* @typedef {Brk.MetricsTree_Distribution_UtxoCohorts_All_Relative} AllRelativePattern
*/
/**
* @template T
* @typedef {Brk.BlockCountPattern<T>} BlockCountPattern
*/
/**
* @template T
* @typedef {Brk.FullnessPattern<T>} FullnessPattern
*/
/**
* @template T
* @typedef {Brk.FeeRatePattern<T>} FeeRatePattern
*/
/**
* @template T
* @typedef {Brk.MetricEndpointBuilder<T>} MetricEndpoint
*/
/**
* @template T
* @typedef {Brk.DollarsPattern<T>} SizePattern
*/
/**
* @template T
* @typedef {Brk.CountPattern2<T>} CountStatsPattern
*/
/**
* @typedef {Brk.MetricsTree_Blocks_Size} BlockSizePattern
*/
/**
* Stats pattern union - accepts both CountStatsPattern and BlockSizePattern
* @typedef {CountStatsPattern<any> | BlockSizePattern} AnyStatsPattern
*/
/**
*
* @typedef {InstanceType<typeof BrkClient>["INDEXES"]} Indexes
* @typedef {Indexes[number]} IndexName
* @typedef {InstanceType<typeof BrkClient>["POOL_ID_TO_POOL_NAME"]} PoolIdToPoolName
* @typedef {keyof PoolIdToPoolName} PoolId
*
* Tree branch types
* @typedef {Brk.MetricsTree_Market} Market
* @typedef {Brk.MetricsTree_Market_MovingAverage} MarketMovingAverage
* @typedef {Brk.MetricsTree_Market_Dca} MarketDca
*
* Pattern unions by cohort type
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} UtxoCohortPattern
* @typedef {AddressAmountPattern} AddressCohortPattern
* @typedef {UtxoCohortPattern | AddressCohortPattern} CohortPattern
*
* Relative pattern capability types
* @typedef {RelativePattern | RelativePattern5} RelativeWithMarketCap
* @typedef {RelativePattern2 | RelativePattern5} RelativeWithOwnMarketCap
* @typedef {RelativePattern2 | RelativePattern5 | AllRelativePattern} RelativeWithOwnPnl
* @typedef {GlobalRelativePattern | FullRelativePattern} RelativeWithMarketCap
* @typedef {OwnRelativePattern | FullRelativePattern} RelativeWithOwnMarketCap
* @typedef {OwnRelativePattern | FullRelativePattern | AllRelativePattern} RelativeWithOwnPnl
*
* Capability-based pattern groupings (patterns that have specific properties)
* @typedef {AllUtxoPattern | AgeRangePattern | UtxoAmountPattern} PatternWithRealizedPrice

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

View File

@@ -178,7 +178,7 @@ export function fromBitcoin(colors, pattern, title, color) {
/**
* Create series from a SizePattern ({ sum, cumulative, average, min, max, percentiles })
* @param {Colors} colors
* @param {SizePattern} pattern
* @param {AnyStatsPattern} pattern
* @param {string} title
* @param {Color} [color]
* @returns {AnyFetchedSeriesBlueprint[]}
@@ -241,7 +241,7 @@ export function fromBlockSize(colors, pattern, title, color) {
/**
* Create series from a SizePattern ({ average, sum, cumulative, min, max, percentiles })
* @param {Colors} colors
* @param {SizePattern} pattern
* @param {AnyStatsPattern} pattern
* @param {string} title
* @param {Unit} unit
* @returns {AnyFetchedSeriesBlueprint[]}

View File

@@ -238,8 +238,8 @@
* @property {HistogramSeriesFn} histogram
* @property {(pattern: BlockCountPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockCount
* @property {(pattern: FullnessPattern<any>, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBitcoin
* @property {(pattern: SizePattern, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockSize
* @property {(pattern: SizePattern, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromSizePattern
* @property {(pattern: AnyStatsPattern, title: string, color?: Color) => AnyFetchedSeriesBlueprint[]} fromBlockSize
* @property {(pattern: AnyStatsPattern, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromSizePattern
* @property {(pattern: FullnessPattern<any>, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromFullnessPattern
* @property {(pattern: FeeRatePattern<any>, title: string, unit: Unit) => AnyFetchedSeriesBlueprint[]} fromFeeRatePattern
* @property {(pattern: CoinbasePattern, title: string) => AnyFetchedSeriesBlueprint[]} fromCoinbasePattern

View File

@@ -8,6 +8,9 @@ import { ios, canShare } from "../../utils/env.js";
import { serdeChartableIndex, serdeOptNumber } from "../../utils/serde.js";
import { throttle } from "../../utils/timing.js";
import { Unit } from "../../utils/units.js";
import signals from "../../signals.js";
import { createChartElement } from "../../chart/index.js";
import { webSockets } from "../../utils/ws.js";
const keyPrefix = "chart";
const ONE_BTC_IN_SATS = 100_000_000;
@@ -22,20 +25,12 @@ const CANDLE = "candle";
/**
* @param {Object} args
* @param {Colors} args.colors
* @param {CreateChartElement} args.createChartElement
* @param {Accessor<ChartOption>} args.option
* @param {Signals} args.signals
* @param {WebSockets} args.webSockets
* @param {Resources} args.resources
* @param {BrkClient} args.brk
*/
export function init({
colors,
createChartElement,
option,
signals,
webSockets,
resources,
brk,
}) {
chartElement.append(createShadow("left"));
@@ -44,10 +39,7 @@ export function init({
const { headerElement, headingElement } = createHeader();
chartElement.append(headerElement);
const { index, fieldset } = createIndexSelector({
option,
signals,
});
const { index, fieldset } = createIndexSelector(option);
const TIMERANGE_LS_KEY = signals.createMemo(
() => `chart-timerange-${index()}`,
@@ -77,7 +69,6 @@ export function init({
signals,
colors,
id: "charts",
resources,
brk,
index,
timeScaleSetCallback: (unknownTimeScaleCallback) => {
@@ -525,11 +516,9 @@ export function init({
}
/**
* @param {Object} args
* @param {Accessor<ChartOption>} args.option
* @param {Signals} args.signals
* @param {Accessor<ChartOption>} option
*/
function createIndexSelector({ option, signals }) {
function createIndexSelector(option) {
const choices_ = /** @satisfies {ChartableIndexName[]} */ ([
"timestamp",
"date",

View File

@@ -1,25 +1,7 @@
import { randomFromArray } from "../utils/array.js";
import { explorerElement } from "../utils/elements.js";
/**
* @param {Object} args
* @param {Colors} args.colors
* @param {CreateChartElement} args.createChartElement
* @param {Accessor<ChartOption>} args.option
* @param {Signals} args.signals
* @param {WebSockets} args.webSockets
* @param {Resources} args.resources
* @param {BrkClient} args.brk
*/
export function init({
colors: _colors,
createChartElement: _createChartElement,
option: _option,
signals: _signals,
webSockets: _webSockets,
resources: _resources,
brk: _brk,
}) {
export function init() {
const chain = window.document.createElement("div");
chain.id = "chain";
explorerElement.append(chain);

View File

@@ -18,15 +18,15 @@ import {
numberToUSNumber,
} from "../utils/format.js";
import { serdeDate, serdeOptDate, serdeOptNumber } from "../utils/serde.js";
import signals from "../signals.js";
import { createChartElement } from "../chart/index.js";
import { resources } from "../resources.js";
/**
* @param {Object} args
* @param {Colors} args.colors
* @param {CreateChartElement} args.createChartElement
* @param {Signals} args.signals
* @param {Resources} args.resources
*/
export function init({ colors, createChartElement, signals, resources }) {
export function init({ colors }) {
/**
* @typedef {Object} Frequency
* @property {string} name

View File

@@ -6,14 +6,7 @@ import { tableElement } from "../utils/elements.js";
import { serdeMetrics, serdeString } from "../utils/serde.js";
import { resetParams } from "../utils/url.js";
/**
* @param {Object} args
* @param {Signals} args.signals
* @param {Option} args.option
* @param {Resources} args.resources
* @param {BrkClient} args.brk
*/
export function init({ signals, option, resources, brk }) {
export function init() {
tableElement.innerHTML = "wip, will hopefuly be back soon, sorry !";
// const parent = tableElement;

View File

@@ -25,105 +25,106 @@
/** @typedef {MetricResource<unknown>} AnyMetricResource */
/**
* @typedef {ReturnType<typeof createResources>} Resources
* @typedef {{ createResource: typeof createResource, useMetricEndpoint: typeof useMetricEndpoint }} Resources
*/
import signals from "./signals.js";
/**
* @param {Signals} signals
* Create a generic reactive resource wrapper for any async fetcher
* @template T
* @template {any[]} Args
* @param {(...args: Args) => Promise<T>} fetcher
* @returns {Resource<T>}
*/
export function createResources(signals) {
function createResource(fetcher) {
const owner = signals.getOwner();
return signals.runWithOwner(owner, () => {
const data = signals.createSignal(/** @type {T | null} */ (null));
const loading = signals.createSignal(false);
const error = signals.createSignal(/** @type {Error | null} */ (null));
/**
* Create a generic reactive resource wrapper for any async fetcher
* @template T
* @template {any[]} Args
* @param {(...args: Args) => Promise<T>} fetcher
* @returns {Resource<T>}
*/
function createResource(fetcher) {
return signals.runWithOwner(owner, () => {
const data = signals.createSignal(/** @type {T | null} */ (null));
const loading = signals.createSignal(false);
const error = signals.createSignal(/** @type {Error | null} */ (null));
return {
data,
loading,
error,
/**
* @param {Args} args
*/
async fetch(...args) {
loading.set(true);
error.set(null);
try {
const result = await fetcher(...args);
data.set(() => result);
return result;
} catch (e) {
error.set(e instanceof Error ? e : new Error(String(e)));
return null;
} finally {
loading.set(false);
}
},
};
});
}
/**
* Create a reactive resource wrapper for a MetricEndpoint with multi-range support
* @template T
* @param {MetricEndpoint<T>} endpoint
* @returns {MetricResource<T>}
*/
function useMetricEndpoint(endpoint) {
return signals.runWithOwner(owner, () => {
/** @type {Map<string, RangeState<T>>} */
const ranges = new Map();
return {
data,
loading,
error,
/**
* Get or create range state
* @param {number} [from=-10000]
* @param {number} [to]
* @returns {RangeState<T>}
* @param {Args} args
*/
function range(from = -10000, to) {
const key = `${from}-${to ?? ""}`;
const existing = ranges.get(key);
if (existing) return existing;
/** @type {RangeState<T>} */
const state = {
response: signals.createSignal(/** @type {MetricData<T> | null} */ (null)),
loading: signals.createSignal(false),
};
ranges.set(key, state);
return state;
}
return {
path: endpoint.path,
range,
/**
* Fetch data for a range
* @param {number} [from=-10000]
* @param {number} [to]
*/
async fetch(from = -10000, to) {
const r = range(from, to);
r.loading.set(true);
try {
const result = await endpoint.range(from, to, r.response.set);
return result;
} finally {
r.loading.set(false);
}
},
};
});
}
return { createResource, useMetricEndpoint };
async fetch(...args) {
loading.set(true);
error.set(null);
try {
const result = await fetcher(...args);
data.set(() => result);
return result;
} catch (e) {
error.set(e instanceof Error ? e : new Error(String(e)));
return null;
} finally {
loading.set(false);
}
},
};
});
}
/**
* Create a reactive resource wrapper for a MetricEndpoint with multi-range support
* @template T
* @param {MetricEndpoint<T>} endpoint
* @returns {MetricResource<T>}
*/
function useMetricEndpoint(endpoint) {
const owner = signals.getOwner();
return signals.runWithOwner(owner, () => {
/** @type {Map<string, RangeState<T>>} */
const ranges = new Map();
/**
* Get or create range state
* @param {number} [from=-10000]
* @param {number} [to]
* @returns {RangeState<T>}
*/
function range(from = -10000, to) {
const key = `${from}-${to ?? ""}`;
const existing = ranges.get(key);
if (existing) return existing;
/** @type {RangeState<T>} */
const state = {
response: signals.createSignal(
/** @type {MetricData<T> | null} */ (null),
),
loading: signals.createSignal(false),
};
ranges.set(key, state);
return state;
}
return {
path: endpoint.path,
range,
/**
* Fetch data for a range
* @param {number} [start=-10000]
* @param {number} [end]
*/
async fetch(start = -10000, end) {
const r = range(start, end);
r.loading.set(true);
try {
const result = await endpoint
.slice(start, end)
.fetch(r.response.set);
return result;
} finally {
r.loading.set(false);
}
},
};
});
}
export const resources = { createResource, useMetricEndpoint };

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) {
/**
* @template T
* @param {(callback: (value: T) => void) => WebSocket} creator
*/
function createWebsocket(creator) {
let ws = /** @type {WebSocket | null} */ (null);
function createWebsocket(creator) {
let ws = /** @type {WebSocket | null} */ (null);
const live = signals.createSignal(false);
const latest = signals.createSignal(/** @type {T | null} */ (null));
const live = signals.createSignal(false);
const latest = signals.createSignal(/** @type {T | null} */ (null));
function reinitWebSocket() {
if (!ws || ws.readyState === ws.CLOSED) {
console.log("ws: reinit");
resource.open();
}
function reinitWebSocket() {
if (!ws || ws.readyState === ws.CLOSED) {
console.log("ws: reinit");
resource.open();
}
}
function reinitWebSocketIfDocumentNotHidden() {
!window.document.hidden && reinitWebSocket();
}
function reinitWebSocketIfDocumentNotHidden() {
!window.document.hidden && reinitWebSocket();
}
const resource = {
live,
latest,
open() {
ws = creator((value) => latest.set(() => value));
const resource = {
live,
latest,
open() {
ws = creator((value) => latest.set(() => value));
ws.addEventListener("open", () => {
console.log("ws: open");
live.set(true);
});
ws.addEventListener("open", () => {
console.log("ws: open");
live.set(true);
});
ws.addEventListener("close", () => {
console.log("ws: close");
live.set(false);
});
window.document.addEventListener(
"visibilitychange",
reinitWebSocketIfDocumentNotHidden,
);
window.document.addEventListener("online", reinitWebSocket);
},
close() {
ws?.close();
window.document.removeEventListener(
"visibilitychange",
reinitWebSocketIfDocumentNotHidden,
);
window.document.removeEventListener("online", reinitWebSocket);
ws.addEventListener("close", () => {
console.log("ws: close");
live.set(false);
ws = null;
},
};
});
return resource;
}
/**
* @param {(candle: CandlestickData) => void} callback
*/
function krakenCandleWebSocketCreator(callback) {
const ws = new WebSocket("wss://ws.kraken.com/v2");
ws.addEventListener("open", () => {
ws.send(
JSON.stringify({
method: "subscribe",
params: {
channel: "ohlc",
symbol: ["BTC/USD"],
interval: 1440,
},
}),
window.document.addEventListener(
"visibilitychange",
reinitWebSocketIfDocumentNotHidden,
);
});
ws.addEventListener("message", (message) => {
const result = JSON.parse(message.data);
window.document.addEventListener("online", reinitWebSocket);
},
close() {
ws?.close();
window.document.removeEventListener(
"visibilitychange",
reinitWebSocketIfDocumentNotHidden,
);
window.document.removeEventListener("online", reinitWebSocket);
live.set(false);
ws = null;
},
};
if (result.channel !== "ohlc") return;
return resource;
}
const { interval_begin, open, high, low, close } = result.data.at(-1);
/**
* @param {(candle: CandlestickData) => void} callback
*/
function krakenCandleWebSocketCreator(callback) {
const ws = new WebSocket("wss://ws.kraken.com/v2");
/** @type {CandlestickData} */
const candle = {
// index: -1,
time: new Date(interval_begin).valueOf() / 1000,
open: Number(open),
high: Number(high),
low: Number(low),
close: Number(close),
};
candle && callback({ ...candle });
});
return ws;
}
/** @type {ReturnType<typeof createWebsocket<CandlestickData>>} */
const kraken1dCandle = createWebsocket((callback) =>
krakenCandleWebSocketCreator(callback),
);
kraken1dCandle.open();
signals.createEffect(kraken1dCandle.latest, (latest) => {
if (latest) {
const close = latest.close;
console.log("close:", close);
window.document.title = `${latest.close.toLocaleString("en-us")} | ${
window.location.host
}`;
}
ws.addEventListener("open", () => {
ws.send(
JSON.stringify({
method: "subscribe",
params: {
channel: "ohlc",
symbol: ["BTC/USD"],
interval: 1440,
},
}),
);
});
return {
kraken1dCandle,
};
ws.addEventListener("message", (message) => {
const result = JSON.parse(message.data);
if (result.channel !== "ohlc") return;
const { interval_begin, open, high, low, close } = result.data.at(-1);
/** @type {CandlestickData} */
const candle = {
// index: -1,
time: new Date(interval_begin).valueOf() / 1000,
open: Number(open),
high: Number(high),
low: Number(low),
close: Number(close),
};
candle && callback({ ...candle });
});
return ws;
}
/** @typedef {ReturnType<typeof createWebSockets>} WebSockets */
/** @type {ReturnType<typeof createWebsocket<CandlestickData>>} */
const kraken1dCandle = createWebsocket((callback) =>
krakenCandleWebSocketCreator(callback),
);
kraken1dCandle.open();
export const webSockets = {
kraken1dCandle,
};
/** @typedef {typeof webSockets} WebSockets */