global: snapshot

This commit is contained in:
nym21
2026-02-13 13:54:09 +01:00
parent b779edc0d6
commit 80b2c636b0
53 changed files with 1819 additions and 1184 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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));
}

View File

@@ -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;
}

View File

@@ -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`);

View File

@@ -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;

View File

@@ -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]

View 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();
});
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

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