global: snapshot

This commit is contained in:
nym21
2025-12-20 11:48:37 +01:00
parent 4a0ce6337f
commit 4b910ceaa7
6 changed files with 87 additions and 80 deletions
+51 -44
View File
@@ -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
+3 -4
View File
@@ -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
+3 -4
View File
@@ -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)
+2 -2
View File
@@ -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