mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-23 00:48:11 -07:00
144 lines
4.7 KiB
JavaScript
144 lines
4.7 KiB
JavaScript
import { brk } from "../../../utils/client.js";
|
|
|
|
const WORKER_URL = import.meta.resolve("./worker.js");
|
|
const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
|
|
/** @typedef {{ path: string, name: string, endpoint: string, suggestedUnit?: string, matchedQuery?: string, score?: number }} CatalogMetric */
|
|
|
|
/** @param {unknown} value */
|
|
function isMetric(value) {
|
|
if (!value || typeof value !== "object") return false;
|
|
const by = /** @type {{ by?: Record<string, unknown> }} */ (value).by;
|
|
return Boolean(
|
|
by &&
|
|
Object.values(by).some(
|
|
(endpoint) =>
|
|
endpoint &&
|
|
typeof endpoint === "object" &&
|
|
typeof /** @type {{ fetch?: unknown }} */ (endpoint).fetch === "function",
|
|
),
|
|
);
|
|
}
|
|
|
|
/** @param {string} path */
|
|
function normalizePath(path) {
|
|
const normalized = path
|
|
.trim()
|
|
.replace(/^brk\.series\./, "")
|
|
.replace(/^series\./, "");
|
|
const keys = normalized.split(".").filter(Boolean);
|
|
|
|
if (!keys.length || keys.some((key) => FORBIDDEN_KEYS.has(key))) {
|
|
throw new Error(`Invalid metric path: ${path}`);
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
/** @param {typeof brk} client @param {string} path */
|
|
export function resolveMetric(client, path) {
|
|
const keys = normalizePath(path);
|
|
/** @type {unknown} */
|
|
let value = client.series;
|
|
|
|
for (const key of keys) {
|
|
if (!value || typeof value !== "object" || !Object.hasOwn(value, key)) {
|
|
throw new Error(`Metric not found: ${path}`);
|
|
}
|
|
value = /** @type {Record<string, unknown>} */ (value)[key];
|
|
}
|
|
|
|
if (!isMetric(value)) throw new Error(`Metric not found: ${path}`);
|
|
return /** @type {TimeframeMetric} */ (value);
|
|
}
|
|
|
|
/** @param {string} path */
|
|
export function createMetric(path) {
|
|
resolveMetric(brk, path);
|
|
return (/** @type {typeof brk} */ client) => resolveMetric(client, path);
|
|
}
|
|
|
|
class MetricIndex {
|
|
/** @type {Worker | undefined} */
|
|
#worker;
|
|
|
|
/** @type {Map<string, { resolve: (value: any) => void, reject: (error: Error) => void, onProgress?: () => void }>} */
|
|
#pending = new Map();
|
|
|
|
/** @param {"search" | "categories" | "byName" | "variants"} type @param {Record<string, unknown>} data @param {(() => void) | undefined} [onProgress] */
|
|
request(type, data, onProgress) {
|
|
this.#ensureWorker();
|
|
const id = crypto.randomUUID();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.#pending.set(id, { resolve, reject, onProgress });
|
|
this.#worker?.postMessage({ id, type, data });
|
|
});
|
|
}
|
|
|
|
#ensureWorker() {
|
|
if (this.#worker) return;
|
|
this.#worker = new Worker(WORKER_URL, { type: "module" });
|
|
this.#worker.addEventListener("message", this.#handleMessage);
|
|
this.#worker.addEventListener("error", this.#handleError);
|
|
}
|
|
|
|
terminate() {
|
|
const error = new Error("Metric search stopped");
|
|
for (const request of this.#pending.values()) request.reject(error);
|
|
this.#pending.clear();
|
|
this.#worker?.terminate();
|
|
this.#worker = undefined;
|
|
}
|
|
|
|
/** @param {MessageEvent} event */
|
|
#handleMessage = (event) => {
|
|
const message = event.data;
|
|
const request = this.#pending.get(message.id);
|
|
if (!request) return;
|
|
|
|
if (message.status === "progress") {
|
|
request.onProgress?.();
|
|
return;
|
|
}
|
|
|
|
this.#pending.delete(message.id);
|
|
if (message.status === "complete") request.resolve(message.data);
|
|
else request.reject(new Error(message.data));
|
|
};
|
|
|
|
/** @param {ErrorEvent} event */
|
|
#handleError = (event) => {
|
|
const error = new Error(event.message || "The metric index failed");
|
|
for (const request of this.#pending.values()) request.reject(error);
|
|
this.#pending.clear();
|
|
this.#worker?.terminate();
|
|
this.#worker = undefined;
|
|
};
|
|
}
|
|
|
|
const index = new MetricIndex();
|
|
|
|
/** @param {string[]} queries @param {number} [limit] @param {string[]} [prefixes] @param {(() => void) | undefined} [onProgress] @returns {Promise<CatalogMetric[]>} */
|
|
export function searchMetrics(queries, limit = 16, prefixes = [], onProgress) {
|
|
return index.request("search", { queries, limit, prefixes }, onProgress);
|
|
}
|
|
|
|
/** @returns {Promise<{ path: string, label: string, count: number, examples: string[] }[]>} */
|
|
export function metricCategories() {
|
|
return index.request("categories", {});
|
|
}
|
|
|
|
/** @param {string} name @returns {Promise<CatalogMetric | undefined>} */
|
|
export function metricByName(name) {
|
|
return index.request("byName", { name });
|
|
}
|
|
|
|
/** @param {{ name: string }} metric @param {string} query @returns {Promise<{ totalSeries: number, groups: { family: string, examples: string[] }[], series: CatalogMetric[] } | undefined>} */
|
|
export function metricVariants(metric, query = "") {
|
|
return index.request("variants", { name: metric.name, query });
|
|
}
|
|
|
|
export function terminateMetricIndex() {
|
|
index.terminate();
|
|
}
|