mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 06:39:58 -07:00
global: snapshot
This commit is contained in:
@@ -472,10 +472,48 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
lastStamp: null,
|
||||
/** @type {string | null} */
|
||||
lastTimeStamp: null,
|
||||
/** @type {number | null} */
|
||||
lastVersion: null,
|
||||
/** @type {number | null} */
|
||||
lastTimeVersion: null,
|
||||
/** @type {VoidFunction | null} */
|
||||
fetch: null,
|
||||
/** @type {((data: MetricData<number>) => void) | null} */
|
||||
onTime: null,
|
||||
reset() {
|
||||
this.hasData = false;
|
||||
this.lastTime = -Infinity;
|
||||
this.lastStamp = null;
|
||||
this.lastTimeStamp = null;
|
||||
this.lastVersion = null;
|
||||
this.lastTimeVersion = null;
|
||||
},
|
||||
/**
|
||||
* @param {string | null} valuesStamp
|
||||
* @param {string} timeStamp
|
||||
* @param {number | null} valuesVersion
|
||||
* @param {number} timeVersion
|
||||
*/
|
||||
shouldProcess(valuesStamp, timeStamp, valuesVersion, timeVersion) {
|
||||
if (
|
||||
valuesStamp === this.lastStamp &&
|
||||
timeStamp === this.lastTimeStamp
|
||||
)
|
||||
return false;
|
||||
// Version change means data was recomputed, needs full reload
|
||||
if (
|
||||
valuesVersion !== this.lastVersion ||
|
||||
timeVersion !== this.lastTimeVersion
|
||||
) {
|
||||
this.hasData = false;
|
||||
this.lastTime = -Infinity;
|
||||
}
|
||||
this.lastStamp = valuesStamp;
|
||||
this.lastTimeStamp = timeStamp;
|
||||
this.lastVersion = valuesVersion;
|
||||
this.lastTimeVersion = timeVersion;
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {AnySeries} */
|
||||
@@ -523,8 +561,7 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
/** @param {ChartableIndex} idx */
|
||||
function setupIndexEffect(idx) {
|
||||
// Reset data state for new index
|
||||
state.hasData = false;
|
||||
state.lastTime = -Infinity;
|
||||
state.reset();
|
||||
state.fetch = null;
|
||||
|
||||
const _valuesEndpoint = metric.by[idx];
|
||||
@@ -663,17 +700,21 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
let valuesData = null;
|
||||
/** @type {string | null} */
|
||||
let valuesStamp = null;
|
||||
/** @type {number | null} */
|
||||
let valuesVersion = null;
|
||||
|
||||
function tryProcess() {
|
||||
if (seriesGeneration !== generation) return;
|
||||
if (!timeData || !valuesData) return;
|
||||
if (
|
||||
valuesStamp === state.lastStamp &&
|
||||
timeData.stamp === state.lastTimeStamp
|
||||
!state.shouldProcess(
|
||||
valuesStamp,
|
||||
timeData.stamp,
|
||||
valuesVersion,
|
||||
timeData.version,
|
||||
)
|
||||
)
|
||||
return;
|
||||
state.lastStamp = valuesStamp;
|
||||
state.lastTimeStamp = timeData.stamp;
|
||||
if (timeData.data.length && valuesData.length) {
|
||||
processData(timeData.data, valuesData);
|
||||
}
|
||||
@@ -691,12 +732,14 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
if (cachedValues) {
|
||||
valuesData = cachedValues.data;
|
||||
valuesStamp = cachedValues.stamp;
|
||||
valuesVersion = cachedValues.version;
|
||||
tryProcess();
|
||||
}
|
||||
await valuesEndpoint.slice(-10000).fetch((result) => {
|
||||
cache.set(valuesEndpoint.path, result);
|
||||
valuesData = result.data;
|
||||
valuesStamp = result.stamp;
|
||||
valuesVersion = result.version;
|
||||
tryProcess();
|
||||
});
|
||||
}
|
||||
@@ -1253,6 +1296,7 @@ export function createChart({ parent, brk, fitContent }) {
|
||||
persisted.set(value);
|
||||
applyScaleForUnit(paneIndex);
|
||||
},
|
||||
toTitle: (c) => (c === "lin" ? "Linear scale" : "Logarithmic scale"),
|
||||
});
|
||||
td.append(radios);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ export function createLegend() {
|
||||
|
||||
/** @param {HTMLElement} el */
|
||||
function captureScroll(el) {
|
||||
el.addEventListener("wheel", (e) => e.stopPropagation());
|
||||
el.addEventListener("touchstart", (e) => e.stopPropagation());
|
||||
el.addEventListener("touchmove", (e) => e.stopPropagation());
|
||||
el.addEventListener("wheel", (e) => e.stopPropagation(), { passive: true });
|
||||
el.addEventListener("touchstart", (e) => e.stopPropagation(), { passive: true });
|
||||
el.addEventListener("touchmove", (e) => e.stopPropagation(), { passive: true });
|
||||
}
|
||||
captureScroll(items);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { webSockets } from "./utils/ws.js";
|
||||
import * as formatters from "./utils/format.js";
|
||||
import { initPrice, onPrice } from "./utils/price.js";
|
||||
import { brk } from "./client.js";
|
||||
import { stringToId } from "./utils/format.js";
|
||||
import { onFirstIntersection, getElementById, isHidden } from "./utils/dom.js";
|
||||
import { initOptions } from "./options/full.js";
|
||||
import {
|
||||
@@ -105,9 +106,11 @@ function initFrameSelectors() {
|
||||
}
|
||||
initFrameSelectors();
|
||||
|
||||
webSockets.kraken1dCandle.onLatest((latest) => {
|
||||
console.log("close:", latest.close);
|
||||
window.document.title = `${latest.close.toLocaleString("en-us")} | ${window.location.host}`;
|
||||
initPrice(brk);
|
||||
|
||||
onPrice((price) => {
|
||||
console.log("close:", price);
|
||||
window.document.title = `${price.toLocaleString("en-us")} | ${window.location.host}`;
|
||||
});
|
||||
|
||||
const options = initOptions();
|
||||
@@ -118,7 +121,7 @@ window.addEventListener("popstate", (_event) => {
|
||||
|
||||
while (path.length) {
|
||||
const id = path.shift();
|
||||
const res = folder.find((v) => id === formatters.stringToId(v.name));
|
||||
const res = folder.find((v) => id === stringToId(v.name));
|
||||
if (!res) throw "Option not found";
|
||||
if (path.length >= 1) {
|
||||
if (!("tree" in res)) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { serdeChartableIndex } from "../utils/serde.js";
|
||||
import { Unit } from "../utils/units.js";
|
||||
import { createChart } from "../chart/index.js";
|
||||
import { colors } from "../utils/colors.js";
|
||||
import { webSockets } from "../utils/ws.js";
|
||||
import { latestPrice, onPrice } from "../utils/price.js";
|
||||
import { brk } from "../client.js";
|
||||
|
||||
const ONE_BTC_IN_SATS = 100_000_000;
|
||||
@@ -63,8 +63,8 @@ export function init() {
|
||||
}
|
||||
|
||||
function updatePriceWithLatest() {
|
||||
const latest = webSockets.kraken1dCandle.latest();
|
||||
if (!latest) return;
|
||||
const latest = latestPrice();
|
||||
if (latest === null) return;
|
||||
|
||||
const priceSeries = chart.panes[0].series[0];
|
||||
const unit = chart.panes[0].unit;
|
||||
@@ -78,8 +78,8 @@ export function init() {
|
||||
// Convert to sats if needed
|
||||
const close =
|
||||
unit === Unit.sats
|
||||
? Math.floor(ONE_BTC_IN_SATS / latest.close)
|
||||
: latest.close;
|
||||
? Math.floor(ONE_BTC_IN_SATS / latest)
|
||||
: latest;
|
||||
|
||||
priceSeries.update({ ...last, close });
|
||||
}
|
||||
@@ -101,7 +101,7 @@ export function init() {
|
||||
};
|
||||
|
||||
// Live price update listener
|
||||
webSockets.kraken1dCandle.onLatest(updatePriceWithLatest);
|
||||
onPrice(updatePriceWithLatest);
|
||||
}
|
||||
|
||||
const ALL_CHOICES = /** @satisfies {ChartableIndexName[]} */ ([
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
*
|
||||
* @import { Color } from "./utils/colors.js"
|
||||
*
|
||||
* @import { WebSockets } from "./utils/ws.js"
|
||||
*
|
||||
* @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType, AnyFetchedSeriesBlueprint, TableOption, ExplorerOption, UrlOption, PartialOptionsGroup, OptionsGroup, PartialOptionsTree, UtxoCohortObject, AddressCohortObject, CohortObject, CohortGroupObject, FetchedLineSeriesBlueprint, FetchedBaselineSeriesBlueprint, FetchedHistogramSeriesBlueprint, FetchedDotsBaselineSeriesBlueprint, PatternAll, PatternFull, PatternWithAdjusted, PatternWithPercentiles, PatternBasic, PatternBasicWithMarketCap, PatternBasicWithoutMarketCap, PatternWithoutRelative, CohortAll, CohortFull, CohortWithAdjusted, CohortWithPercentiles, CohortBasic, CohortBasicWithMarketCap, CohortBasicWithoutMarketCap, CohortWithoutRelative, CohortAddress, CohortLongTerm, CohortAgeRange, CohortMinAge, CohortGroupFull, CohortGroupWithAdjusted, CohortGroupWithPercentiles, CohortGroupLongTerm, CohortGroupAgeRange, CohortGroupBasic, CohortGroupBasicWithMarketCap, CohortGroupBasicWithoutMarketCap, CohortGroupWithoutRelative, CohortGroupMinAge, CohortGroupAddress, UtxoCohortGroupObject, AddressCohortGroupObject, FetchedDotsSeriesBlueprint, FetchedCandlestickSeriesBlueprint, FetchedPriceSeriesBlueprint, AnyPricePattern, AnyValuePattern } from "./options/partial.js"
|
||||
*
|
||||
*
|
||||
|
||||
@@ -1,24 +1,3 @@
|
||||
/**
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
*/
|
||||
export function range(start, end) {
|
||||
const range = [];
|
||||
while (start <= end) {
|
||||
range.push(start);
|
||||
start += 1;
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T[]} array
|
||||
*/
|
||||
export function randomFromArray(array) {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed Object.entries that preserves key types
|
||||
* @template {Record<string, any>} T
|
||||
|
||||
@@ -244,20 +244,20 @@ export const colors = {
|
||||
long: palette.fuchsia,
|
||||
},
|
||||
|
||||
scriptType: seq([
|
||||
"p2pk65",
|
||||
"p2pk33",
|
||||
"p2pkh",
|
||||
"p2ms",
|
||||
"p2sh",
|
||||
"p2wpkh",
|
||||
"p2wsh",
|
||||
"p2tr",
|
||||
"p2a",
|
||||
"opreturn",
|
||||
"unknown",
|
||||
"empty",
|
||||
]),
|
||||
scriptType: {
|
||||
p2pk65: palette.rose,
|
||||
p2pk33: palette.pink,
|
||||
p2pkh: palette.orange,
|
||||
p2ms: palette.teal,
|
||||
p2sh: palette.green,
|
||||
p2wpkh: palette.red,
|
||||
p2wsh: palette.yellow,
|
||||
p2tr: palette.cyan,
|
||||
p2a: palette.indigo,
|
||||
opreturn: palette.purple,
|
||||
unknown: palette.violet,
|
||||
empty: palette.fuchsia,
|
||||
},
|
||||
|
||||
arr: paletteArr,
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
const ONE_DAY_IN_MS = 1000 * 60 * 60 * 24;
|
||||
|
||||
export function todayUTC() {
|
||||
const today = new Date();
|
||||
return new Date(
|
||||
Date.UTC(
|
||||
today.getUTCFullYear(),
|
||||
today.getUTCMonth(),
|
||||
today.getUTCDate(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} date
|
||||
*/
|
||||
export function dateToDateIndex(date) {
|
||||
if (
|
||||
date.getUTCFullYear() === 2009 &&
|
||||
date.getUTCMonth() === 0 &&
|
||||
date.getUTCDate() === 3
|
||||
)
|
||||
return 0;
|
||||
return differenceBetweenDates(date, new Date("2009-01-09"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} start
|
||||
* @param {Date} end
|
||||
*/
|
||||
export function createDateRange(start, end) {
|
||||
const dates = /** @type {Date[]} */ ([]);
|
||||
let currentDate = new Date(start);
|
||||
while (currentDate <= end) {
|
||||
dates.push(new Date(currentDate));
|
||||
currentDate.setUTCDate(currentDate.getUTCDate() + 1);
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} date1
|
||||
* @param {Date} date2
|
||||
*/
|
||||
export function differenceBetweenDates(date1, date2) {
|
||||
return Math.abs(date1.valueOf() - date2.valueOf()) / ONE_DAY_IN_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} date1
|
||||
* @param {Date} date2
|
||||
*/
|
||||
export function roundedDifferenceBetweenDates(date1, date2) {
|
||||
return Math.round(differenceBetweenDates(date1, date2));
|
||||
}
|
||||
@@ -176,6 +176,7 @@ export function createLabeledInput({
|
||||
* @param {(value: T) => void} [args.onChange]
|
||||
* @param {(choice: T) => string} [args.toKey]
|
||||
* @param {(choice: T) => string} [args.toLabel]
|
||||
* @param {(choice: T) => string | undefined} [args.toTitle]
|
||||
*/
|
||||
export function createRadios({
|
||||
id,
|
||||
@@ -184,6 +185,7 @@ export function createRadios({
|
||||
onChange,
|
||||
toKey = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
|
||||
toLabel = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
|
||||
toTitle,
|
||||
}) {
|
||||
const field = window.document.createElement("div");
|
||||
field.classList.add("field");
|
||||
@@ -208,6 +210,7 @@ export function createRadios({
|
||||
inputName: fieldId,
|
||||
inputValue: choiceKey,
|
||||
inputChecked: choiceKey === initialKey,
|
||||
title: toTitle?.(choice),
|
||||
type: "radio",
|
||||
});
|
||||
|
||||
@@ -314,31 +317,3 @@ export function createHeader(title = "", level = 1) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {string} Name
|
||||
* @template {string} Value
|
||||
* @template {Value | {name: Name; value: Value}} T
|
||||
* @param {T} arg
|
||||
*/
|
||||
export function createOption(arg) {
|
||||
const option = window.document.createElement("option");
|
||||
if (typeof arg === "object") {
|
||||
option.value = arg.value;
|
||||
option.innerText = arg.name;
|
||||
} else {
|
||||
option.value = arg;
|
||||
option.innerText = arg;
|
||||
}
|
||||
return option;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param {'left' | 'bottom' | 'top' | 'right'} position
|
||||
*/
|
||||
export function createShadow(position) {
|
||||
const div = window.document.createElement("div");
|
||||
div.classList.add(`shadow-${position}`);
|
||||
return div;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,6 @@ export const asideElement = getElementById("aside");
|
||||
export const searchElement = getElementById("search");
|
||||
export const navElement = getElementById("nav");
|
||||
export const chartElement = getElementById("chart");
|
||||
export const tableElement = getElementById("table");
|
||||
export const explorerElement = getElementById("explorer");
|
||||
export const simulationElement = getElementById("simulation");
|
||||
|
||||
export const asideLabelElement = getElementById("aside-selector-label");
|
||||
export const navLabelElement = getElementById(`nav-selector-label`);
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
export const localhost = window.location.hostname === "localhost";
|
||||
export const standalone =
|
||||
"standalone" in window.navigator && !!window.navigator.standalone;
|
||||
export const userAgent = navigator.userAgent.toLowerCase();
|
||||
export const isChrome = userAgent.includes("chrome");
|
||||
export const safari = userAgent.includes("safari");
|
||||
export const safariOnly = safari && !isChrome;
|
||||
export const macOS = userAgent.includes("mac os");
|
||||
export const iphone = userAgent.includes("iphone");
|
||||
export const ipad = userAgent.includes("ipad");
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
const iphone = userAgent.includes("iphone");
|
||||
const ipad = userAgent.includes("ipad");
|
||||
export const ios = iphone || ipad;
|
||||
export const canShare = "canShare" in navigator;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @param {number} [digits]
|
||||
* @param {Intl.NumberFormatOptions} [options]
|
||||
*/
|
||||
export function numberToUSNumber(value, digits, options) {
|
||||
function numberToUSNumber(value, digits, options) {
|
||||
return value.toLocaleString("en-us", {
|
||||
...options,
|
||||
minimumFractionDigits: digits,
|
||||
@@ -11,19 +11,6 @@ export function numberToUSNumber(value, digits, options) {
|
||||
});
|
||||
}
|
||||
|
||||
export const numberToDollars = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
export const numberToPercentage = new Intl.NumberFormat("en-US", {
|
||||
style: "percent",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @param {0 | 2} [digits]
|
||||
|
||||
36
website/scripts/utils/price.js
Normal file
36
website/scripts/utils/price.js
Normal file
@@ -0,0 +1,36 @@
|
||||
let _latest = /** @type {number | null} */ (null);
|
||||
|
||||
/** @type {Set<(price: number) => void>} */
|
||||
const listeners = new Set();
|
||||
|
||||
/** @param {(price: number) => void} callback */
|
||||
export function onPrice(callback) {
|
||||
listeners.add(callback);
|
||||
if (_latest !== null) callback(_latest);
|
||||
return () => listeners.delete(callback);
|
||||
}
|
||||
|
||||
export function latestPrice() {
|
||||
return _latest;
|
||||
}
|
||||
|
||||
/** @param {BrkClient} brk */
|
||||
export function initPrice(brk) {
|
||||
async function poll() {
|
||||
try {
|
||||
const price = await brk.getLivePrice();
|
||||
if (price !== _latest) {
|
||||
_latest = price;
|
||||
listeners.forEach((cb) => cb(price));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("price poll:", e);
|
||||
}
|
||||
}
|
||||
|
||||
poll();
|
||||
setInterval(poll, 5_000);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
!document.hidden && poll();
|
||||
});
|
||||
}
|
||||
@@ -1,96 +1,3 @@
|
||||
const localhost = window.location.hostname === "localhost";
|
||||
console.log({ localhost });
|
||||
|
||||
export const serdeString = {
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return v;
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return v;
|
||||
},
|
||||
};
|
||||
|
||||
export const serdeMetrics = {
|
||||
/**
|
||||
* @param {Metric[]} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return v.join(",");
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return /** @type {Metric[]} */ (v.split(","));
|
||||
},
|
||||
};
|
||||
|
||||
export const serdeNumber = {
|
||||
/**
|
||||
* @param {number} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return String(v);
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return Number(v);
|
||||
},
|
||||
};
|
||||
|
||||
export const serdeOptNumber = {
|
||||
/**
|
||||
* @param {number | null} v
|
||||
*/
|
||||
serialize(v) {
|
||||
return v !== null ? String(v) : "";
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return v ? Number(v) : null;
|
||||
},
|
||||
};
|
||||
|
||||
export const serdeDate = {
|
||||
/**
|
||||
* @param {Date} date
|
||||
*/
|
||||
serialize(date) {
|
||||
return date.toString();
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return new Date(v);
|
||||
},
|
||||
};
|
||||
|
||||
export const serdeOptDate = {
|
||||
/**
|
||||
* @param {Date | null} date
|
||||
*/
|
||||
serialize(date) {
|
||||
return date !== null ? date.toString() : "";
|
||||
},
|
||||
/**
|
||||
* @param {string} v
|
||||
*/
|
||||
deserialize(v) {
|
||||
return new Date(v);
|
||||
},
|
||||
};
|
||||
|
||||
export const serdeBool = {
|
||||
/**
|
||||
* @param {boolean} v
|
||||
|
||||
@@ -18,7 +18,7 @@ export function onChange(callback) {
|
||||
}
|
||||
|
||||
/** @param {boolean} value */
|
||||
export function setDark(value) {
|
||||
function setDark(value) {
|
||||
if (dark === value) return;
|
||||
dark = value;
|
||||
apply(value);
|
||||
|
||||
@@ -67,35 +67,6 @@ export function writeParam(key, value) {
|
||||
replaceHistory({ urlParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
export function removeParam(key) {
|
||||
writeParam(key, undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
export function readBoolParam(key) {
|
||||
const param = readParam(key);
|
||||
if (param) {
|
||||
return param === "true" || param === "1";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
export function readNumberParam(key) {
|
||||
const param = readParam(key);
|
||||
if (param) {
|
||||
return Number(param);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
/**
|
||||
* @template T
|
||||
* @param {(callback: (value: T) => void) => WebSocket} creator
|
||||
*/
|
||||
function createWebsocket(creator) {
|
||||
let ws = /** @type {WebSocket | null} */ (null);
|
||||
let _live = false;
|
||||
let _latest = /** @type {T | null} */ (null);
|
||||
|
||||
/** @type {Set<(value: T) => void>} */
|
||||
const listeners = new Set();
|
||||
|
||||
function reinitWebSocket() {
|
||||
if (!ws || ws.readyState === ws.CLOSED) {
|
||||
console.log("ws: reinit");
|
||||
resource.open();
|
||||
}
|
||||
}
|
||||
|
||||
function reinitWebSocketIfDocumentNotHidden() {
|
||||
!window.document.hidden && reinitWebSocket();
|
||||
}
|
||||
|
||||
const resource = {
|
||||
live() {
|
||||
return _live;
|
||||
},
|
||||
latest() {
|
||||
return _latest;
|
||||
},
|
||||
/** @param {(value: T) => void} callback */
|
||||
onLatest(callback) {
|
||||
listeners.add(callback);
|
||||
return () => listeners.delete(callback);
|
||||
},
|
||||
open() {
|
||||
ws = creator((value) => {
|
||||
_latest = value;
|
||||
listeners.forEach((cb) => cb(value));
|
||||
});
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
console.log("ws: open");
|
||||
_live = true;
|
||||
});
|
||||
|
||||
ws.addEventListener("close", () => {
|
||||
console.log("ws: close");
|
||||
_live = false;
|
||||
});
|
||||
|
||||
window.document.addEventListener(
|
||||
"visibilitychange",
|
||||
reinitWebSocketIfDocumentNotHidden,
|
||||
);
|
||||
|
||||
window.document.addEventListener("online", reinitWebSocket);
|
||||
},
|
||||
close() {
|
||||
ws?.close();
|
||||
window.document.removeEventListener(
|
||||
"visibilitychange",
|
||||
reinitWebSocketIfDocumentNotHidden,
|
||||
);
|
||||
window.document.removeEventListener("online", reinitWebSocket);
|
||||
_live = 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,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
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: /** @type {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();
|
||||
|
||||
export const webSockets = {
|
||||
kraken1dCandle,
|
||||
};
|
||||
/** @typedef {typeof webSockets} WebSockets */
|
||||
Reference in New Issue
Block a user