mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 06:01:57 -07:00
global: snapshot
This commit is contained in:
@@ -6,7 +6,7 @@ use std::path::Path;
|
||||
|
||||
use brk_types::{Index, TreeNode};
|
||||
|
||||
use super::{ClientMetadata, Endpoint, IndexSetPattern, PatternField, StructuralPattern, get_node_fields, to_camel_case, to_pascal_case, to_snake_case};
|
||||
use super::{ClientMetadata, Endpoint, IndexSetPattern, PatternField, StructuralPattern, get_node_fields, to_camel_case, to_pascal_case};
|
||||
|
||||
/// Generate JavaScript + JSDoc client from metadata and OpenAPI endpoints
|
||||
pub fn generate_javascript_client(
|
||||
@@ -50,6 +50,14 @@ fn generate_base_client(output: &mut String) {
|
||||
* @property {{number}} [timeout] - Request timeout in milliseconds
|
||||
*/
|
||||
|
||||
const _isBrowser = typeof window !== 'undefined' && 'caches' in window;
|
||||
const _runIdle = (fn) => (globalThis.requestIdleCallback ?? setTimeout)(fn);
|
||||
|
||||
/** @type {{Promise<Cache | null>}} */
|
||||
const _cachePromise = _isBrowser
|
||||
? caches.open('__BRK_CLIENT__').catch(() => null)
|
||||
: Promise.resolve(null);
|
||||
|
||||
/**
|
||||
* Custom error class for BRK client errors
|
||||
*/
|
||||
@@ -75,73 +83,73 @@ class MetricNode {{
|
||||
* @param {{string}} path
|
||||
*/
|
||||
constructor(client, path) {{
|
||||
this.client = client;
|
||||
this.path = path;
|
||||
this._client = client;
|
||||
this._path = path;
|
||||
}}
|
||||
|
||||
/**
|
||||
* Fetch all data points for this metric.
|
||||
* @returns {{Promise<T[]>}}
|
||||
* @param {{(value: T[]) => void}} [onUpdate] - Called when data is available (may be called twice: cache then fresh)
|
||||
* @returns {{Promise<T[] | null>}}
|
||||
*/
|
||||
async get() {{
|
||||
return this.client.get(this.path);
|
||||
get(onUpdate) {{
|
||||
return this._client.get(this._path, onUpdate);
|
||||
}}
|
||||
|
||||
/**
|
||||
* Fetch data points within a date range.
|
||||
* @param {{string}} from
|
||||
* @param {{string}} to
|
||||
* @returns {{Promise<T[]>}}
|
||||
* Fetch data points within a range.
|
||||
* @param {{string | number}} from
|
||||
* @param {{string | number}} to
|
||||
* @param {{(value: T[]) => void}} [onUpdate] - Called when data is available (may be called twice: cache then fresh)
|
||||
* @returns {{Promise<T[] | null>}}
|
||||
*/
|
||||
async getRange(from, to) {{
|
||||
return this.client.get(`${{this.path}}?from=${{from}}&to=${{to}}`);
|
||||
getRange(from, to, onUpdate) {{
|
||||
return this._client.get(`${{this._path}}?from=${{from}}&to=${{to}}`, onUpdate);
|
||||
}}
|
||||
}}
|
||||
|
||||
/**
|
||||
* Base HTTP client for making requests
|
||||
* Base HTTP client for making requests with caching support
|
||||
*/
|
||||
class BrkClientBase {{
|
||||
/**
|
||||
* @param {{BrkClientOptions|string}} options
|
||||
*/
|
||||
constructor(options) {{
|
||||
if (typeof options === 'string') {{
|
||||
this.baseUrl = options;
|
||||
this.timeout = 30000;
|
||||
}} else {{
|
||||
this.baseUrl = options.baseUrl;
|
||||
this.timeout = options.timeout || 30000;
|
||||
}}
|
||||
const isString = typeof options === 'string';
|
||||
this.baseUrl = isString ? options : options.baseUrl;
|
||||
this.timeout = isString ? 5000 : (options.timeout ?? 5000);
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a GET request
|
||||
* Make a GET request with stale-while-revalidate caching
|
||||
* @template T
|
||||
* @param {{string}} path
|
||||
* @returns {{Promise<T>}}
|
||||
* @param {{(value: T) => void}} [onUpdate] - Called when data is available
|
||||
* @returns {{Promise<T | null>}}
|
||||
*/
|
||||
async get(path) {{
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||
async get(path, onUpdate) {{
|
||||
const url = `${{this.baseUrl}}${{path}}`;
|
||||
const cache = await _cachePromise;
|
||||
const cachedRes = await cache?.match(url);
|
||||
const cachedJson = cachedRes ? await cachedRes.json() : null;
|
||||
|
||||
if (cachedJson) onUpdate?.(cachedJson);
|
||||
if (!globalThis.navigator?.onLine) return cachedJson;
|
||||
|
||||
try {{
|
||||
const response = await fetch(`${{this.baseUrl}}${{path}}`, {{
|
||||
signal: controller.signal,
|
||||
}});
|
||||
const res = await fetch(url, {{ signal: AbortSignal.timeout(this.timeout) }});
|
||||
if (!res.ok) throw new BrkError(`HTTP ${{res.status}}`, res.status);
|
||||
if (cachedRes?.headers.get('ETag') === res.headers.get('ETag')) return cachedJson;
|
||||
|
||||
if (!response.ok) {{
|
||||
throw new BrkError(`HTTP error: ${{response.status}}`, response.status);
|
||||
}}
|
||||
|
||||
return response.json();
|
||||
}} catch (error) {{
|
||||
if (error.name === 'AbortError') {{
|
||||
throw new BrkError('Request timeout');
|
||||
}}
|
||||
throw error;
|
||||
}} finally {{
|
||||
clearTimeout(timeoutId);
|
||||
const cloned = res.clone();
|
||||
const json = await res.json();
|
||||
onUpdate?.(json);
|
||||
if (cache) _runIdle(() => cache.put(url, cloned));
|
||||
return json;
|
||||
}} catch (e) {{
|
||||
if (cachedJson) return cachedJson;
|
||||
throw e;
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
@@ -183,7 +191,7 @@ fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
|
||||
|
||||
for (i, index) in pattern.indexes.iter().enumerate() {
|
||||
let field_name = index_to_camel_case(index);
|
||||
let path_segment = index.serialize_short();
|
||||
let path_segment = index.serialize_long();
|
||||
let comma = if i < pattern.indexes.len() - 1 { "," } else { "" };
|
||||
writeln!(
|
||||
output,
|
||||
@@ -197,10 +205,9 @@ fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an Index to a camelCase field name (e.g., DateIndex -> byDate)
|
||||
/// Convert an Index to a camelCase field name (e.g., DateIndex -> byDateIndex)
|
||||
fn index_to_camel_case(index: &Index) -> String {
|
||||
let short = index.serialize_short();
|
||||
format!("by{}", to_pascal_case(&to_snake_case(short)))
|
||||
format!("by{}", to_pascal_case(index.serialize_long()))
|
||||
}
|
||||
|
||||
/// Generate structural pattern factory functions
|
||||
|
||||
@@ -136,7 +136,7 @@ fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
|
||||
|
||||
for index in &pattern.indexes {
|
||||
let field_name = index_to_snake_case(index);
|
||||
let path_segment = index.serialize_short();
|
||||
let path_segment = index.serialize_long();
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: MetricNode[T] = MetricNode(client, f'{{base_path}}/{}')",
|
||||
@@ -148,10 +148,9 @@ fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an Index to a snake_case field name (e.g., DateIndex -> by_date)
|
||||
/// Convert an Index to a snake_case field name (e.g., DateIndex -> by_date_index)
|
||||
fn index_to_snake_case(index: &Index) -> String {
|
||||
let short = index.serialize_short();
|
||||
format!("by_{}", to_snake_case(short))
|
||||
format!("by_{}", to_snake_case(index.serialize_long()))
|
||||
}
|
||||
|
||||
/// Generate structural pattern classes
|
||||
|
||||
@@ -203,7 +203,7 @@ fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
|
||||
|
||||
for index in &pattern.indexes {
|
||||
let field_name = index_to_field_name(index);
|
||||
let path_segment = index.serialize_short();
|
||||
let path_segment = index.serialize_long();
|
||||
writeln!(
|
||||
output,
|
||||
" {}: MetricNode::new(client, format!(\"{{base_path}}/{}\")),",
|
||||
@@ -218,10 +218,9 @@ fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an Index to a snake_case field name (e.g., DateIndex -> by_date)
|
||||
/// Convert an Index to a snake_case field name (e.g., DateIndex -> by_date_index)
|
||||
fn index_to_field_name(index: &Index) -> String {
|
||||
let short = index.serialize_short();
|
||||
format!("by_{}", to_snake_case(short))
|
||||
format!("by_{}", to_snake_case(index.serialize_long()))
|
||||
}
|
||||
|
||||
/// Generate pattern structs (those appearing 2+ times)
|
||||
|
||||
@@ -334,11 +334,11 @@ fn collect_indexes_from_tree(
|
||||
fn generate_index_set_name(indexes: &BTreeSet<Index>) -> String {
|
||||
if indexes.len() == 1 {
|
||||
let index = indexes.iter().next().unwrap();
|
||||
return format!("{}Accessor", to_pascal_case(index.serialize_short()));
|
||||
return format!("{}Accessor", to_pascal_case(index.serialize_long()));
|
||||
}
|
||||
|
||||
// For multiple indexes, create a descriptive name
|
||||
let names: Vec<&str> = indexes.iter().map(|i| i.serialize_short()).collect();
|
||||
let names: Vec<&str> = indexes.iter().map(|i| i.serialize_long()).collect();
|
||||
format!("{}Accessor", to_pascal_case(&names.join("_")))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@ use brk_types::{LoadedAddressData, OutputType, TypeIndex};
|
||||
use super::super::address::AddressTypeToTypeIndexMap;
|
||||
use super::{EmptyAddressDataWithSource, LoadedAddressDataWithSource, WithAddressDataSource};
|
||||
|
||||
/// Source of an address in lookup - reports where the data came from.
|
||||
/// Tracking status of an address - determines cohort update strategy.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum AddressSource {
|
||||
pub enum TrackingStatus {
|
||||
/// Brand new address (never seen before)
|
||||
New,
|
||||
/// Loaded from disk (has existing balance)
|
||||
Loaded,
|
||||
/// Was empty (zero balance), now receiving
|
||||
FromEmpty,
|
||||
/// Already tracked in a cohort (has existing balance)
|
||||
Tracked,
|
||||
/// Was in empty cache, now rejoining a cohort
|
||||
WasEmpty,
|
||||
}
|
||||
|
||||
/// Context for looking up and storing address data during block processing.
|
||||
@@ -27,7 +27,7 @@ impl<'a> AddressLookup<'a> {
|
||||
&mut self,
|
||||
output_type: OutputType,
|
||||
type_index: TypeIndex,
|
||||
) -> (&mut LoadedAddressDataWithSource, AddressSource) {
|
||||
) -> (&mut LoadedAddressDataWithSource, TrackingStatus) {
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
let map = self.loaded.get_mut(output_type).unwrap();
|
||||
@@ -40,36 +40,38 @@ impl<'a> AddressLookup<'a> {
|
||||
// - If wrapper is New AND funded_txo_count == 0: hasn't received yet,
|
||||
// was just created in process_outputs this block → New
|
||||
// - If wrapper is New AND funded_txo_count > 0: received in previous
|
||||
// block but still in cache (no flush) → Loaded
|
||||
// - If wrapper is FromLoaded/FromEmpty: loaded from storage → use wrapper
|
||||
let source = match entry.get() {
|
||||
// block but still in cache (no flush) → Tracked
|
||||
// - If wrapper is FromLoaded: loaded from storage → Tracked
|
||||
// - If wrapper is FromEmpty AND utxo_count == 0: still empty → WasEmpty
|
||||
// - If wrapper is FromEmpty AND utxo_count > 0: already received → Tracked
|
||||
let status = match entry.get() {
|
||||
WithAddressDataSource::New(data) => {
|
||||
if data.funded_txo_count == 0 {
|
||||
AddressSource::New
|
||||
TrackingStatus::New
|
||||
} else {
|
||||
AddressSource::Loaded
|
||||
TrackingStatus::Tracked
|
||||
}
|
||||
}
|
||||
WithAddressDataSource::FromLoaded(..) => AddressSource::Loaded,
|
||||
WithAddressDataSource::FromLoaded(..) => TrackingStatus::Tracked,
|
||||
WithAddressDataSource::FromEmpty(_, data) => {
|
||||
if data.utxo_count() == 0 {
|
||||
AddressSource::FromEmpty
|
||||
TrackingStatus::WasEmpty
|
||||
} else {
|
||||
AddressSource::Loaded
|
||||
TrackingStatus::Tracked
|
||||
}
|
||||
}
|
||||
};
|
||||
(entry.into_mut(), source)
|
||||
(entry.into_mut(), status)
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
if let Some(empty_data) =
|
||||
self.empty.get_mut(output_type).unwrap().remove(&type_index)
|
||||
{
|
||||
return (entry.insert(empty_data.into()), AddressSource::FromEmpty);
|
||||
return (entry.insert(empty_data.into()), TrackingStatus::WasEmpty);
|
||||
}
|
||||
(
|
||||
entry.insert(WithAddressDataSource::New(LoadedAddressData::default())),
|
||||
AddressSource::New,
|
||||
TrackingStatus::New,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
use super::super::address::AddressTypeToVec;
|
||||
use super::super::cohorts::AddressCohorts;
|
||||
use super::lookup::{AddressLookup, AddressSource};
|
||||
use super::lookup::{AddressLookup, TrackingStatus};
|
||||
|
||||
pub fn process_received(
|
||||
received_data: AddressTypeToVec<(TypeIndex, Sats)>,
|
||||
@@ -31,23 +31,23 @@ pub fn process_received(
|
||||
}
|
||||
|
||||
for (type_index, (total_value, output_count)) in aggregated {
|
||||
let (addr_data, source) = lookup.get_or_create_for_receive(output_type, type_index);
|
||||
let (addr_data, status) = lookup.get_or_create_for_receive(output_type, type_index);
|
||||
|
||||
match source {
|
||||
AddressSource::New => {
|
||||
match status {
|
||||
TrackingStatus::New => {
|
||||
*addr_count.get_mut(output_type).unwrap() += 1;
|
||||
}
|
||||
AddressSource::FromEmpty => {
|
||||
TrackingStatus::WasEmpty => {
|
||||
*addr_count.get_mut(output_type).unwrap() += 1;
|
||||
*empty_addr_count.get_mut(output_type).unwrap() -= 1;
|
||||
}
|
||||
AddressSource::Loaded => {}
|
||||
TrackingStatus::Tracked => {}
|
||||
}
|
||||
|
||||
let is_new_entry = matches!(source, AddressSource::New | AddressSource::FromEmpty);
|
||||
let is_new_entry = matches!(status, TrackingStatus::New | TrackingStatus::WasEmpty);
|
||||
|
||||
if is_new_entry {
|
||||
// New/from-empty address - just add to cohort
|
||||
// New/was-empty address - just add to cohort
|
||||
addr_data.receive_outputs(total_value, price, output_count);
|
||||
cohorts
|
||||
.amount_range
|
||||
|
||||
Reference in New Issue
Block a user