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

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 */