use std::fmt::Write;
use crate::{
ClientConstants, ClientMetadata, CohortConstants, GenericSyntax, IndexSetPattern,
JavaScriptSyntax, StructuralPattern, camel_case_keys, format_json,
generate_parameterized_field, to_camel_case,
};
pub fn generate_base_client(output: &mut String) {
writeln!(
output,
r#"/**
* @typedef {{Object}} BrkClientOptions
* @property {{string}} baseUrl - Base URL for the API
* @property {{number}} [timeout] - Request timeout in milliseconds
* @property {{string|boolean}} [browserCache] - Enable browser Cache API with default name (true), custom name (string), or disable (false). No effect in Node.js. Default: true
* @property {{number|boolean}} [memCache] - In-memory parsed-response cache size (LRU). true/undefined → 1000, false/0 → disabled. Lets 304 responses skip the JSON parse entirely. Default: 1000
*/
const _isBrowser = typeof window !== 'undefined' && 'caches' in window;
const _runIdle = (/** @type {{VoidFunction}} */ fn) => (globalThis.requestIdleCallback ?? setTimeout)(fn);
const _defaultBrowserCacheName = '__BRK_CLIENT__';
const _DEFAULT_MEM_CACHE_SIZE = 1000;
/** @template T @typedef {{{{ etag: string | null, value: T }}}} _MemEntry */
/** @param {{*}} v */
const _addCamelGetters = (v) => {{
if (Array.isArray(v)) {{ v.forEach(_addCamelGetters); return v; }}
if (v && typeof v === 'object' && v.constructor === Object) {{
for (const k in v) {{
if (k.includes('_')) {{
const c = k.replace(/_([a-z])/g, (_, l) => l.toUpperCase());
if (!(c in v)) Object.defineProperty(v, c, {{ get() {{ return this[k]; }} }});
}}
_addCamelGetters(v[k]);
}}
}}
return v;
}};
/**
* @param {{string|boolean|undefined}} option
* @returns {{Promise<Cache | null>}}
*/
const _openBrowserCache = (option) => {{
if (!_isBrowser || option === false) return Promise.resolve(null);
const name = typeof option === 'string' ? option : _defaultBrowserCacheName;
return caches.open(name).catch(() => null);
}};
/**
* Custom error class for BRK client errors
*/
class BrkError extends Error {{
/**
* @param {{string}} message
* @param {{number}} [status]
*/
constructor(message, status) {{
super(message);
this.name = 'BrkError';
this.status = status;
}}
}}
// Date conversion constants and helpers
const _GENESIS = new Date(2009, 0, 3); // day1 0, week1 0
const _DAY_ONE = new Date(2009, 0, 9); // day1 1 (6 day gap after genesis)
const _MS_PER_DAY = 86400000;
const _MS_PER_WEEK = 7 * _MS_PER_DAY;
const _EPOCH_MS = 1230768000000;
const _DATE_INDEXES = new Set([
'minute10', 'minute30',
'hour1', 'hour4', 'hour12',
'day1', 'day3', 'week1',
'month1', 'month3', 'month6',
'year1', 'year10',
]);
/** @param {{number}} months @returns {{globalThis.Date}} */
const _addMonths = (months) => new Date(2009, months, 1);
/**
* Convert an index value to a Date for date-based indexes.
* @param {{Index}} index - The index type
* @param {{number}} i - The index value
* @returns {{globalThis.Date}}
*/
function indexToDate(index, i) {{
switch (index) {{
case 'minute10': return new Date(_EPOCH_MS + i * 600000);
case 'minute30': return new Date(_EPOCH_MS + i * 1800000);
case 'hour1': return new Date(_EPOCH_MS + i * 3600000);
case 'hour4': return new Date(_EPOCH_MS + i * 14400000);
case 'hour12': return new Date(_EPOCH_MS + i * 43200000);
case 'day1': return i === 0 ? _GENESIS : new Date(_DAY_ONE.getTime() + (i - 1) * _MS_PER_DAY);
case 'day3': return new Date(_EPOCH_MS - 86400000 + i * 259200000);
case 'week1': return new Date(_GENESIS.getTime() + i * _MS_PER_WEEK);
case 'month1': return _addMonths(i);
case 'month3': return _addMonths(i * 3);
case 'month6': return _addMonths(i * 6);
case 'year1': return new Date(2009 + i, 0, 1);
case 'year10': return new Date(2009 + i * 10, 0, 1);
default: throw new Error(`${{index}} is not a date-based index`);
}}
}}
/**
* Convert a Date to an index value for date-based indexes.
* Returns the floor index (latest index whose date is <= the given date).
* @param {{Index}} index - The index type
* @param {{globalThis.Date}} d - The date to convert
* @returns {{number}}
*/
function dateToIndex(index, d) {{
const ms = d.getTime();
switch (index) {{
case 'minute10': return Math.floor((ms - _EPOCH_MS) / 600000);
case 'minute30': return Math.floor((ms - _EPOCH_MS) / 1800000);
case 'hour1': return Math.floor((ms - _EPOCH_MS) / 3600000);
case 'hour4': return Math.floor((ms - _EPOCH_MS) / 14400000);
case 'hour12': return Math.floor((ms - _EPOCH_MS) / 43200000);
case 'day1': {{
if (ms < _DAY_ONE.getTime()) return 0;
return 1 + Math.floor((ms - _DAY_ONE.getTime()) / _MS_PER_DAY);
}}
case 'day3': return Math.floor((ms - _EPOCH_MS + 86400000) / 259200000);
case 'week1': return Math.floor((ms - _GENESIS.getTime()) / _MS_PER_WEEK);
case 'month1': return (d.getFullYear() - 2009) * 12 + d.getMonth();
case 'month3': return (d.getFullYear() - 2009) * 4 + Math.floor(d.getMonth() / 3);
case 'month6': return (d.getFullYear() - 2009) * 2 + Math.floor(d.getMonth() / 6);
case 'year1': return d.getFullYear() - 2009;
case 'year10': return Math.floor((d.getFullYear() - 2009) / 10);
default: throw new Error(`${{index}} is not a date-based index`);
}}
}}
/**
* Wrap raw series data with helper methods.
* @template T
* @param {{SeriesData<T>}} raw - Raw JSON response
* @returns {{DateSeriesData<T>}}
*/
function _wrapSeriesData(raw) {{
const {{ index, start, end, data }} = raw;
const _dateBased = _DATE_INDEXES.has(index);
return /** @type {{DateSeriesData<T>}} */ ({{
...raw,
isDateBased: _dateBased,
indexes() {{
/** @type {{number[]}} */
const result = [];
for (let i = start; i < end; i++) result.push(i);
return result;
}},
keys() {{
return this.indexes();
}},
entries() {{
/** @type {{Array<[number, T]>}} */
const result = [];
for (let i = 0; i < data.length; i++) result.push([start + i, data[i]]);
return result;
}},
toMap() {{
/** @type {{Map<number, T>}} */
const map = new Map();
for (let i = 0; i < data.length; i++) map.set(start + i, data[i]);
return map;
}},
*[Symbol.iterator]() {{
for (let i = 0; i < data.length; i++) yield /** @type {{[number, T]}} */ ([start + i, data[i]]);
}},
// DateSeriesData methods (only meaningful for date-based indexes)
dates() {{
/** @type {{globalThis.Date[]}} */
const result = [];
for (let i = start; i < end; i++) result.push(indexToDate(index, i));
return result;
}},
dateEntries() {{
/** @type {{Array<[globalThis.Date, T]>}} */
const result = [];
for (let i = 0; i < data.length; i++) result.push([indexToDate(index, start + i), data[i]]);
return result;
}},
toDateMap() {{
/** @type {{Map<globalThis.Date, T>}} */
const map = new Map();
for (let i = 0; i < data.length; i++) map.set(indexToDate(index, start + i), data[i]);
return map;
}},
}});
}}
/**
* @template T
* @typedef {{Object}} SeriesDataBase
* @property {{number}} version - Version of the series data
* @property {{Index}} index - The index type used for this query
* @property {{string}} type - Value type (e.g. "f32", "u64", "Sats")
* @property {{number}} start - Start index (inclusive)
* @property {{number}} end - End index (exclusive)
* @property {{string}} stamp - ISO 8601 timestamp of when the response was generated
* @property {{T[]}} data - The series data
* @property {{boolean}} isDateBased - Whether this series uses a date-based index
* @property {{() => number[]}} indexes - Get index numbers
* @property {{() => number[]}} keys - Get keys as index numbers (alias for indexes)
* @property {{() => Array<[number, T]>}} entries - Get [index, value] pairs
* @property {{() => Map<number, T>}} toMap - Convert to Map<index, value>
*/
/** @template T @typedef {{SeriesDataBase<T> & Iterable<[number, T]>}} SeriesData */
/**
* @template T
* @typedef {{Object}} DateSeriesDataExtras
* @property {{() => globalThis.Date[]}} dates - Get dates for each data point
* @property {{() => Array<[globalThis.Date, T]>}} dateEntries - Get [date, value] pairs
* @property {{() => Map<globalThis.Date, T>}} toDateMap - Convert to Map<date, value>
*/
/** @template T @typedef {{SeriesData<T> & DateSeriesDataExtras<T>}} DateSeriesData */
/** @typedef {{SeriesData<any>}} AnySeriesData */
/** @template T @typedef {{(onfulfilled?: (value: SeriesData<T>) => any, onrejected?: (reason: Error) => never) => Promise<SeriesData<T>>}} Thenable */
/** @template T @typedef {{(onfulfilled?: (value: DateSeriesData<T>) => any, onrejected?: (reason: Error) => never) => Promise<DateSeriesData<T>>}} DateThenable */
/**
* @template T
* @typedef {{Object}} SeriesEndpoint
* @property {{(index: number) => SingleItemBuilder<T>}} get - Get single item at index
* @property {{(start?: number, end?: number) => RangeBuilder<T>}} slice - Slice by index
* @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 {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch all data
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
* @property {{() => Promise<number>}} len - Get total number of data points
* @property {{() => Promise<Version>}} version - Get the current version of the series
* @property {{Thenable<T>}} then - Thenable (await endpoint)
* @property {{string}} path - The endpoint path
*/
/**
* @template T
* @typedef {{Object}} DateSeriesEndpoint
* @property {{(index: number | globalThis.Date) => DateSingleItemBuilder<T>}} get - Get single item at index or Date
* @property {{(start?: number | globalThis.Date, end?: number | globalThis.Date) => DateRangeBuilder<T>}} slice - Slice by index or Date
* @property {{(n: number) => DateRangeBuilder<T>}} first - Get first n items
* @property {{(n: number) => DateRangeBuilder<T>}} last - Get last n items
* @property {{(n: number) => DateSkippedBuilder<T>}} skip - Skip first n items, chain with take()
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch all data
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
* @property {{() => Promise<number>}} len - Get total number of data points
* @property {{() => Promise<Version>}} version - Get the current version of the series
* @property {{DateThenable<T>}} then - Thenable (await endpoint)
* @property {{string}} path - The endpoint path
*/
/** @typedef {{SeriesEndpoint<any>}} AnySeriesEndpoint */
/** @template T @typedef {{Object}} SingleItemBuilder
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch the item
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateSingleItemBuilder
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch the item
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} SkippedBuilder
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items after skipped position
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch from skipped position to end
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateSkippedBuilder
* @property {{(n: number) => DateRangeBuilder<T>}} take - Take n items after skipped position
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch from skipped position to end
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} RangeBuilder
* @property {{(onValue?: (value: SeriesData<T>) => void) => Promise<SeriesData<T>>}} fetch - Fetch the range
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateRangeBuilder
* @property {{(onValue?: (value: DateSeriesData<T>) => void) => Promise<DateSeriesData<T>>}} fetch - Fetch the range
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/**
* @template T
* @typedef {{Object}} SeriesPattern
* @property {{string}} name - The series name
* @property {{Readonly<Partial<Record<Index, SeriesEndpoint<T>>>>}} by - Index endpoints as lazy getters
* @property {{() => readonly Index[]}} indexes - Get the list of available indexes
* @property {{(index: Index) => SeriesEndpoint<T>|undefined}} get - Get an endpoint for a specific index
*/
/** @typedef {{SeriesPattern<any>}} AnySeriesPattern */
/**
* Create a series endpoint builder with typestate pattern.
* @template T
* @param {{BrkClient}} client
* @param {{string}} name - The series vec name
* @param {{Index}} index - The index name
* @returns {{DateSeriesEndpoint<T>}}
*/
function _endpoint(client, name, index) {{
const p = `/api/series/${{name}}/${{index}}`;
/**
* @param {{number}} [start]
* @param {{number}} [end]
* @param {{string}} [format]
* @returns {{string}}
*/
const buildPath = (start, end, format) => {{
const params = new URLSearchParams();
if (start !== undefined) params.set('start', String(start));
if (end !== undefined) params.set('end', String(end));
if (format) params.set('format', format);
const query = params.toString();
return query ? `${{p}}?${{query}}` : p;
}};
/**
* @param {{number}} [start]
* @param {{number}} [end]
* @returns {{DateRangeBuilder<T>}}
*/
const rangeBuilder = (start, end) => ({{
fetch(onValue) {{ return client._fetchSeriesData(buildPath(start, end), onValue); }},
fetchCsv() {{ return client.getText(buildPath(start, end, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
/**
* @param {{number}} idx
* @returns {{DateSingleItemBuilder<T>}}
*/
const singleItemBuilder = (idx) => ({{
fetch(onValue) {{ return client._fetchSeriesData(buildPath(idx, idx + 1), onValue); }},
fetchCsv() {{ return client.getText(buildPath(idx, idx + 1, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
/**
* @param {{number}} start
* @returns {{DateSkippedBuilder<T>}}
*/
const skippedBuilder = (start) => ({{
take(n) {{ return rangeBuilder(start, start + n); }},
fetch(onValue) {{ return client._fetchSeriesData(buildPath(start, undefined), onValue); }},
fetchCsv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
/** @type {{DateSeriesEndpoint<T>}} */
const endpoint = {{
get(idx) {{ if (idx instanceof Date) idx = dateToIndex(index, idx); return singleItemBuilder(idx); }},
slice(start, end) {{
if (start instanceof Date) start = dateToIndex(index, start);
if (end instanceof Date) end = dateToIndex(index, end);
return rangeBuilder(start, end);
}},
first(n) {{ return rangeBuilder(undefined, n); }},
last(n) {{ return n === 0 ? rangeBuilder(undefined, 0) : rangeBuilder(-n, undefined); }},
skip(n) {{ return skippedBuilder(n); }},
fetch(onValue) {{ return client._fetchSeriesData(buildPath(), onValue); }},
fetchCsv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
len() {{ return client.getSeriesLen(name, index); }},
version() {{ return client.getSeriesVersion(name, index); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
get path() {{ return p; }},
}};
return endpoint;
}}
/**
* Base HTTP client for making requests with caching support
*/
class BrkClientBase {{
/**
* @param {{BrkClientOptions|string}} options
*/
constructor(options) {{
const isString = typeof options === 'string';
const rawUrl = isString ? options : options.baseUrl;
this.baseUrl = rawUrl.endsWith('/') ? rawUrl.slice(0, -1) : rawUrl;
this.timeout = isString ? 5000 : (options.timeout ?? 5000);
/** @type {{Promise<Cache | null>}} */
this._browserCachePromise = _openBrowserCache(isString ? undefined : options.browserCache);
/** @type {{Cache | null}} */
this._browserCache = null;
this._browserCachePromise.then(c => this._browserCache = c);
const memOpt = isString ? undefined : options.memCache;
this._memCacheMax = memOpt === false || memOpt === 0
? 0
: (typeof memOpt === 'number' ? memOpt : _DEFAULT_MEM_CACHE_SIZE);
/** @type {{Map<string, _MemEntry<unknown>>}} */
this._memCache = new Map();
}}
/**
* @template T
* @param {{string}} key
* @returns {{_MemEntry<T> | undefined}}
*/
_memGet(key) {{
if (!this._memCacheMax) return undefined;
const hit = this._memCache.get(key);
if (!hit) return undefined;
this._memCache.delete(key);
this._memCache.set(key, hit);
return /** @type {{_MemEntry<T>}} */ (hit);
}}
/**
* @param {{string}} key
* @param {{string | null}} etag
* @param {{unknown}} value
*/
_memSet(key, etag, value) {{
if (!this._memCacheMax) return;
if (this._memCache.has(key)) this._memCache.delete(key);
else if (this._memCache.size >= this._memCacheMax) {{
const oldest = this._memCache.keys().next().value;
if (oldest !== undefined) this._memCache.delete(oldest);
}}
this._memCache.set(key, {{ etag, value }});
}}
/**
* @param {{string}} path
* @param {{{{ signal?: AbortSignal }}}} [options]
* @returns {{Promise<Response>}}
*/
async get(path, {{ signal }} = {{}}) {{
const url = `${{this.baseUrl}}${{path}}`;
const signals = [AbortSignal.timeout(this.timeout)];
if (signal) signals.push(signal);
const res = await fetch(url, {{ signal: AbortSignal.any(signals) }});
if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status);
return res;
}}
/**
* Make a GET request with layered caching.
*
* Contract:
* - The returned Promise resolves with the **freshest** value (post-revalidation).
* - `onValue` fires once with the freshest value, or twice if a stale snapshot
* could be shown first (stale-while-revalidate). On a 304 there is no second fire.
*
* Layers:
* - L1 (memCache): in-memory parsed values keyed by URL+ETag. Lets 304s skip the parse entirely.
* - L2 (browserCache): Cache API, survives reload and feeds onValue fast on cold start.
*
* @template T
* @param {{string}} path
* @param {{(res: Response) => Promise<T>}} parse - Response body reader
* @param {{{{ onValue?: (value: T) => void, signal?: AbortSignal }}}} [options]
* @returns {{Promise<T>}}
*/
async _getCached(path, parse, {{ onValue, signal }} = {{}}) {{
const url = `${{this.baseUrl}}${{path}}`;
/** @type {{_MemEntry<T> | undefined}} */
const memHit = this._memGet(url);
const browserCache = this._browserCache ?? await this._browserCachePromise;
// L1 fast path: deliver from memCache, revalidate via network.
// ETag match → zero parse, zero clone, zero cache write, no second onValue fire.
if (memHit) {{
if (onValue) onValue(memHit.value);
try {{
const res = await this.get(path, {{ signal }});
const netEtag = res.headers.get('ETag');
if (netEtag && netEtag === memHit.etag) return memHit.value;
const cloned = browserCache ? res.clone() : null;
const value = await parse(res);
this._memSet(url, netEtag, value);
if (onValue) onValue(value);
if (cloned && browserCache) {{
const cache = browserCache;
_runIdle(() => cache.put(url, cloned));
}}
return value;
}} catch {{
return memHit.value;
}}
}}
// L1 miss: race browserCache (stale snapshot) vs network (fresh).
let networkSettled = false;
const stalePromise = onValue && browserCache
? browserCache.match(url).then(async (res) => {{
if (!res || networkSettled) return null;
const value = await parse(res);
if (networkSettled) return value;
this._memSet(url, res.headers.get('ETag'), value);
onValue(value);
return value;
}}).catch(() => null)
: null;
try {{
const res = await this.get(path, {{ signal }});
networkSettled = true;
const netEtag = res.headers.get('ETag');
// Stale won and populated memCache with matching ETag → reuse, skip parse + second onValue.
const populated = /** @type {{_MemEntry<T> | undefined}} */ (this._memGet(url));
if (populated && netEtag && netEtag === populated.etag) return populated.value;
const cloned = browserCache ? res.clone() : null;
const value = await parse(res);
this._memSet(url, netEtag, value);
if (onValue) onValue(value);
if (cloned && browserCache) {{
const cache = browserCache;
_runIdle(() => cache.put(url, cloned));
}}
return value;
}} catch (e) {{
const stale = await stalePromise;
if (stale != null) return stale;
throw e;
}}
}}
/**
* Make a GET request expecting a JSON response. Cached and supports `onValue`.
* @template T
* @param {{string}} path
* @param {{{{ onValue?: (value: T) => void, signal?: AbortSignal }}}} [options]
* @returns {{Promise<T>}}
*/
getJson(path, options) {{
return this._getCached(path, async (res) => _addCamelGetters(await res.json()), options);
}}
/**
* Make a GET request expecting a text response (text/plain, text/csv, ...).
* Cached and supports `onValue`, same as `getJson`.
* @param {{string}} path
* @param {{{{ onValue?: (value: string) => void, signal?: AbortSignal }}}} [options]
* @returns {{Promise<string>}}
*/
getText(path, options) {{
return this._getCached(path, (res) => res.text(), options);
}}
/**
* Make a GET request expecting binary data (application/octet-stream).
* Cached and supports `onValue`, same as `getJson`.
* @param {{string}} path
* @param {{{{ onValue?: (value: Uint8Array) => void, signal?: AbortSignal }}}} [options]
* @returns {{Promise<Uint8Array>}}
*/
getBytes(path, options) {{
return this._getCached(path, async (res) => new Uint8Array(await res.arrayBuffer()), options);
}}
/**
* Make a POST request with a string body.
*
* POST responses are uncached and never invoke `onValue` — every call hits
* the network with the same body and returns the upstream response.
*
* @param {{string}} path
* @param {{string}} body
* @param {{{{ signal?: AbortSignal }}}} [options]
* @returns {{Promise<Response>}}
*/
async post(path, body, {{ signal }} = {{}}) {{
const url = `${{this.baseUrl}}${{path}}`;
const signals = [AbortSignal.timeout(this.timeout)];
if (signal) signals.push(signal);
const res = await fetch(url, {{
method: 'POST',
body,
signal: AbortSignal.any(signals),
}});
if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status);
return res;
}}
/**
* Make a POST request expecting a JSON response.
* @template T
* @param {{string}} path
* @param {{string}} body
* @param {{{{ signal?: AbortSignal }}}} [options]
* @returns {{Promise<T>}}
*/
async postJson(path, body, options) {{
const res = await this.post(path, body, options);
return _addCamelGetters(await res.json());
}}
/**
* Make a POST request expecting a text response.
* @param {{string}} path
* @param {{string}} body
* @param {{{{ signal?: AbortSignal }}}} [options]
* @returns {{Promise<string>}}
*/
async postText(path, body, options) {{
const res = await this.post(path, body, options);
return res.text();
}}
/**
* Make a POST request expecting binary data (application/octet-stream).
* @param {{string}} path
* @param {{string}} body
* @param {{{{ signal?: AbortSignal }}}} [options]
* @returns {{Promise<Uint8Array>}}
*/
async postBytes(path, body, options) {{
const res = await this.post(path, body, options);
return new Uint8Array(await res.arrayBuffer());
}}
/**
* Fetch series data and wrap with helper methods (internal)
* @template T
* @param {{string}} path
* @param {{(value: DateSeriesData<T>) => void}} [onValue]
* @returns {{Promise<DateSeriesData<T>>}}
*/
async _fetchSeriesData(path, onValue) {{
const wrappedOnValue = onValue ? (/** @type {{SeriesData<T>}} */ raw) => onValue(_wrapSeriesData(raw)) : undefined;
const raw = await this.getJson(path, {{ onValue: wrappedOnValue }});
return _wrapSeriesData(raw);
}}
}}
/**
* Build series name with suffix.
* @param {{string}} acc - Accumulated prefix
* @param {{string}} s - Series suffix
* @returns {{string}}
*/
const _m = (acc, s) => s ? (acc ? `${{acc}}_${{s}}` : s) : acc;
/**
* Build series name with prefix.
* @param {{string}} prefix - Prefix to prepend
* @param {{string}} acc - Accumulated name
* @returns {{string}}
*/
const _p = (prefix, acc) => acc ? `${{prefix}}_${{acc}}` : prefix;
"#
)
.unwrap();
}
pub fn generate_static_constants(output: &mut String) {
let constants = ClientConstants::collect();
writeln!(output, " VERSION = \"{}\";\n", constants.version).unwrap();
write_static_const(output, "INDEXES", &format_json(&constants.indexes));
write_static_const(
output,
"POOL_ID_TO_POOL_NAME",
&format_json(&constants.pool_map),
);
for (name, value) in CohortConstants::all() {
write_static_const(output, name, &format_json(&camel_case_keys(value)));
}
writeln!(
output,
r#" /**
* Convert an index value to a Date for date-based indexes.
* @param {{Index}} index - The index type
* @param {{number}} i - The index value
* @returns {{globalThis.Date}}
*/
indexToDate(index, i) {{
return indexToDate(index, i);
}}
/**
* Convert a Date to an index value for date-based indexes.
* @param {{Index}} index - The index type
* @param {{globalThis.Date}} d - The date to convert
* @returns {{number}}
*/
dateToIndex(index, d) {{
return dateToIndex(index, d);
}}
"#
)
.unwrap();
}
fn indent_json_const(json: &str) -> String {
json.lines()
.enumerate()
.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();
}
pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
if patterns.is_empty() {
return;
}
writeln!(output, "// Index group constants and factory\n").unwrap();
for (i, pattern) in patterns.iter().enumerate() {
write!(output, "const _i{} = /** @type {{const}} */ ([", i + 1).unwrap();
for (j, index) in pattern.indexes.iter().enumerate() {
if j > 0 {
write!(output, ", ").unwrap();
}
write!(output, "\"{}\"", index.name()).unwrap();
}
writeln!(output, "]);").unwrap();
}
writeln!(output).unwrap();
writeln!(
output,
r#"/**
* Generic series pattern factory.
* @template T
* @param {{BrkClient}} client
* @param {{string}} name - The series vec name
* @param {{readonly Index[]}} indexes - The supported indexes
*/
function _mp(client, name, indexes) {{
const by = {{}};
for (const idx of indexes) {{
Object.defineProperty(by, idx, {{
get() {{ return _endpoint(client, name, idx); }},
enumerable: true,
configurable: true
}});
}}
return {{
name,
by,
/** @returns {{readonly Index[]}} */
indexes() {{ return indexes; }},
/** @param {{Index}} index @returns {{SeriesEndpoint<T>|undefined}} */
get(index) {{ return indexes.includes(index) ? _endpoint(client, name, index) : undefined; }}
}};
}}
"#
)
.unwrap();
for (i, pattern) in patterns.iter().enumerate() {
let by_fields: Vec<String> = pattern
.indexes
.iter()
.map(|idx| {
let builder = if idx.is_date_based() {
"DateSeriesEndpoint"
} else {
"SeriesEndpoint"
};
format!("readonly {}: {}<T>", idx.name(), builder)
})
.collect();
let by_type = format!("{{ {} }}", by_fields.join(", "));
writeln!(
output,
"/** @template T @typedef {{{{ name: string, by: {}, indexes: () => readonly Index[], get: (index: Index) => SeriesEndpoint<T>|undefined }}}} {} */",
by_type, pattern.name
)
.unwrap();
writeln!(
output,
"/** @template T @param {{BrkClient}} client @param {{string}} name @returns {{{}<T>}} */",
pattern.name
)
.unwrap();
writeln!(
output,
"function create{}(client, name) {{ return /** @type {{{}<T>}} */ (_mp(client, name, _i{})); }}",
pattern.name,
pattern.name,
i + 1
)
.unwrap();
}
writeln!(output).unwrap();
}
pub fn generate_structural_patterns(
output: &mut String,
patterns: &[StructuralPattern],
metadata: &ClientMetadata,
) {
if patterns.is_empty() {
return;
}
writeln!(output, "// Reusable structural pattern factories\n").unwrap();
for pattern in patterns {
writeln!(output, "/**").unwrap();
if pattern.is_generic {
writeln!(output, " * @template T").unwrap();
}
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,
);
writeln!(
output,
" * @property {{{}}} {}",
js_type,
to_camel_case(&field.name)
)
.unwrap();
}
writeln!(output, " */\n").unwrap();
if !metadata.is_parameterizable(&pattern.name) {
continue;
}
writeln!(output, "/**").unwrap();
writeln!(output, " * Create a {} pattern node", pattern.name).unwrap();
if pattern.is_generic {
writeln!(output, " * @template T").unwrap();
}
writeln!(output, " * @param {{BrkClient}} client").unwrap();
writeln!(output, " * @param {{string}} acc - Accumulated series name").unwrap();
if pattern.is_templated() {
writeln!(output, " * @param {{string}} disc - Discriminator suffix").unwrap();
}
let return_type = if pattern.is_generic {
format!("{}<T>", pattern.name)
} else {
pattern.name.clone()
};
writeln!(output, " * @returns {{{}}}", return_type).unwrap();
writeln!(output, " */").unwrap();
if pattern.is_templated() {
writeln!(
output,
"function create{}(client, acc, disc) {{",
pattern.name
)
.unwrap();
} else {
writeln!(output, "function create{}(client, acc) {{", pattern.name).unwrap();
}
writeln!(output, " return {{").unwrap();
let syntax = JavaScriptSyntax;
for field in &pattern.fields {
generate_parameterized_field(output, &syntax, field, pattern, metadata, " ");
}
writeln!(output, " }};").unwrap();
writeln!(output, "}}\n").unwrap();
}
}