websites: snapshot + todo: init

This commit is contained in:
nym21
2025-07-07 13:16:43 +02:00
parent d31d47eb32
commit bff22b5182
9 changed files with 481 additions and 121 deletions
+86
View File
@@ -0,0 +1,86 @@
# TODO
- __crates__
- _cli_
- check disk space on first launch
- add custom path support for config.toml
- maybe add bitcoind download and launch support
- via: https://github.com/rust-bitcoin/corepc/blob/master/node
- test read/write speed, add warning if too low (<2gb/s)
- pull latest version and notify is out of date
- _computer_
- **add rollback of states (in stateful)**
- add support for per index computation
- fix feerate which is always ZERO due to coinbase transaction
- before computing multiple sources check their length, panic if not equal
- add oracle price dataset (https://utxo.live/oracle/UTXOracle.py)
- add address counts relative to all datasets
- make decade, quarter, year datasets `computed` instead of `eager`
- add 6 months interval datasets to builder
- add revived/sent supply datasets
- add `in-sats` version of all price datasets (average and co)
- add `p2pk` group (sum of `p2pk33` and `p2pk65`)
- add more date ranges (3-6 months and more)
- add puell multiple dataset
- add pi cycle dataset
- add all possible charts from:
- https://mainnet.observer
- https://glassnode.com
- https://checkonchain.com
- https://researchbitcoin.net/exciting-update-coming-to-the-bitcoin-lab/
- https://mempool.space/research
- _indexer_
- parse only the needed block number
- maybe using https://developer.bitcoin.org/reference/rpc/getblockhash.html
- _interface_
- create pagination enum
- from to
- from option<count>
- to option<count>
- page + option<per page> default 1000 max 1000
- from/to/count params dont cap all combinations
- example: from -10,000 count 10, wont work if underlying vec isnt 10k or more long
- _parser_
- save `vec` file instead of `json`
- support lock file, process in read only if already opened in write mode
- if less than X (10 maybe ?) get block using rpc instead of parsing the block files
- _server_
- api
- add extensions support (.json .csv …)
- if format instead of extension then don't download file
- _vec_
- add native lock file support (once it's available in stable rust)
- improve compressed mode (slow reads)
- __docs__
- _README_
- add a comparison table with alternatives
- add contribution section where help is needed
- documentation/mcp/datasets/different front ends
- add faq
- __websites__
- _default_
- explorer
- blocks
- transactions
- addresses
- miners
- maybe xpubs
- charts
- improve some names and colors
- remove `sum` series when it's a duplicate of the `base` (in subsidy for example)
- selected unit sometimes changes when going back end forth
- add support for custom charts
- price scale format depends on unit, hide digits for sats for example (if/when possible)
- table
- pagination
- exports (.json, .csv,…)
- search
- datasets add legend, and keywords ?
- height/address/txid
- api
- add api page with interactivity
- global
- **fix navigation/history**
- move share button to footer ?
- improve behavior when local storage is unavailable
- by having a global state
@@ -24,6 +24,7 @@ where
pub difficultyepoch: ComputedVecBuilder<DifficultyEpoch, T>,
pub monthindex: ComputedVecBuilder<MonthIndex, T>,
pub quarterindex: ComputedVecBuilder<QuarterIndex, T>,
// 6 months
pub yearindex: ComputedVecBuilder<YearIndex, T>,
// TODO: pub halvingepoch: StorableVecGeneator<Halvingepoch, T>,
pub decadeindex: ComputedVecBuilder<DecadeIndex, T>,
@@ -144,9 +145,9 @@ where
indexes: &indexes::Vecs,
starting_indexes: &Indexes,
exit: &Exit,
height: Option<&impl AnyIterableVec<Height, T>>,
height_vec: Option<&impl AnyIterableVec<Height, T>>,
) -> color_eyre::Result<()> {
if let Some(height) = height {
if let Some(height) = height_vec {
self.height_extra
.extend(starting_indexes.height, height, exit)?;
@@ -10,7 +10,9 @@ use brk_vec::{
use crate::{
states::AddressCohortState,
vecs::{
Indexes, fetched, indexes, market,
Indexes, fetched,
grouped::{ComputedVecsFromHeight, StorableVecGeneatorOptions},
indexes, market,
stateful::{
common,
r#trait::{CohortVecs, DynCohortVecs},
@@ -26,9 +28,10 @@ pub struct Vecs {
pub state: AddressCohortState,
pub height_to_address_count: EagerVec<Height, StoredUsize>,
pub inner: common::Vecs,
pub height_to_address_count: EagerVec<Height, StoredUsize>,
pub indexes_to_address_count: ComputedVecsFromHeight<StoredUsize>,
}
impl Vecs {
@@ -60,6 +63,14 @@ impl Vecs {
version + VERSION + Version::ZERO,
format,
)?,
indexes_to_address_count: ComputedVecsFromHeight::forced_import(
path,
&suffix("address_count"),
false,
version + VERSION + Version::ZERO,
format,
StorableVecGeneatorOptions::default().add_last(),
)?,
inner: common::Vecs::forced_import(
path,
cohort_name,
@@ -160,12 +171,24 @@ impl DynCohortVecs for Vecs {
starting_indexes: &Indexes,
exit: &Exit,
) -> color_eyre::Result<()> {
self.indexes_to_address_count.compute_rest(
indexes,
starting_indexes,
exit,
Some(&self.height_to_address_count),
)?;
self.inner
.compute_rest_part1(indexer, indexes, fetched, starting_indexes, exit)
}
fn vecs(&self) -> Vec<&dyn AnyCollectableVec> {
[self.inner.vecs(), vec![&self.height_to_address_count]].concat()
[
self.inner.vecs(),
self.indexes_to_address_count.vecs(),
vec![&self.height_to_address_count],
]
.concat()
}
}
+64 -19
View File
@@ -2,7 +2,7 @@ use std::{cmp::Ordering, collections::BTreeMap, mem, path::Path, thread};
use brk_core::{
AddressData, CheckedSub, DateIndex, Dollars, EmptyAddressData, GroupedByAddressType, Height,
InputIndex, OutputIndex, OutputType, Result, Sats, TypeIndex, Version,
InputIndex, OutputIndex, OutputType, Result, Sats, StoredUsize, TypeIndex, Version,
};
use brk_exit::Exit;
use brk_indexer::Indexer;
@@ -17,6 +17,7 @@ use crate::{
BlockState, SupplyState, Transacted,
stores::Stores,
vecs::{
grouped::ComputedVecsFromHeight,
market,
stateful::{
addresstype_to_addresscount::AddressTypeToAddressCount,
@@ -52,18 +53,19 @@ const VERSION: Version = Version::new(11);
#[derive(Clone)]
pub struct Vecs {
chain_state: StoredVec<Height, SupplyState>,
pub chain_state: StoredVec<Height, SupplyState>,
pub height_to_unspendable_supply: EagerVec<Height, Sats>,
pub indexes_to_unspendable_supply: ComputedValueVecsFromHeight,
pub height_to_opreturn_supply: EagerVec<Height, Sats>,
pub indexes_to_opreturn_supply: ComputedValueVecsFromHeight,
// pub height_to_address_count: EagerVec<Height, StoredUsize>,
// pub height_to_empty_address_count: EagerVec<Height, StoredUsize>,
pub addresstype_to_height_to_address_count: AddressTypeToAddressCountVec,
pub addresstype_to_height_to_empty_address_count: AddressTypeToAddressCountVec,
pub utxo_vecs: utxo_cohorts::Vecs,
pub address_vecs: address_cohorts::Vecs,
pub indexes_to_address_count: ComputedVecsFromHeight<StoredUsize>,
pub indexes_to_empty_address_count: ComputedVecsFromHeight<StoredUsize>,
}
impl Vecs {
@@ -119,18 +121,22 @@ impl Vecs {
StorableVecGeneatorOptions::default().add_last(),
compute_dollars,
)?,
// height_to_address_count: EagerVec::forced_import(
// path,
// "address_count",
// version + VERSION + Version::ZERO,
// format,
// )?,
// height_to_empty_address_count: EagerVec::forced_import(
// path,
// "empty_address_count",
// version + VERSION + Version::ZERO,
// format,
// )?,
indexes_to_address_count: ComputedVecsFromHeight::forced_import(
path,
"address_count",
true,
version + VERSION + Version::ZERO,
format,
StorableVecGeneatorOptions::default().add_last(),
)?,
indexes_to_empty_address_count: ComputedVecsFromHeight::forced_import(
path,
"empty_address_count",
true,
version + VERSION + Version::ZERO,
format,
StorableVecGeneatorOptions::default().add_last(),
)?,
addresstype_to_height_to_address_count: AddressTypeToAddressCountVec::from(
GroupedByAddressType {
p2pk65: EagerVec::forced_import(
@@ -976,6 +982,7 @@ impl Vecs {
.as_mut()
.map(|v| is_date_last_height.then(|| *v.unwrap_get_inner(dateindex)));
let dateindex = is_date_last_height.then_some(dateindex);
separate_utxo_vecs
.into_par_iter()
.map(|(_, v)| v as &mut dyn DynCohortVecs).chain(
@@ -987,7 +994,7 @@ impl Vecs {
v.compute_then_force_push_unrealized_states(
height,
price,
is_date_last_height.then_some(dateindex),
dateindex,
date_price,
exit,
)
@@ -1039,6 +1046,44 @@ impl Vecs {
info!("Computing rest part 1...");
self.indexes_to_address_count.compute_all(
indexer,
indexes,
starting_indexes,
exit,
|v, _, _, starting_indexes, exit| {
v.compute_sum_of_others(
starting_indexes.height,
&self
.addresstype_to_height_to_address_count
.as_typed_vec()
.into_iter()
.map(|(_, v)| v)
.collect::<Vec<_>>(),
exit,
)
},
)?;
self.indexes_to_empty_address_count.compute_all(
indexer,
indexes,
starting_indexes,
exit,
|v, _, _, starting_indexes, exit| {
v.compute_sum_of_others(
starting_indexes.height,
&self
.addresstype_to_height_to_empty_address_count
.as_typed_vec()
.into_iter()
.map(|(_, v)| v)
.collect::<Vec<_>>(),
exit,
)
},
)?;
thread::scope(|scope| {
scope.spawn(|| {
self.utxo_vecs
@@ -1194,6 +1239,8 @@ impl Vecs {
.collect::<Vec<_>>(),
self.indexes_to_unspendable_supply.vecs(),
self.indexes_to_opreturn_supply.vecs(),
self.indexes_to_address_count.vecs(),
self.indexes_to_empty_address_count.vecs(),
self.addresstype_to_height_to_address_count
.as_typed_vec()
.into_iter()
@@ -1207,8 +1254,6 @@ impl Vecs {
vec![
&self.height_to_unspendable_supply,
&self.height_to_opreturn_supply,
// &self.height_to_address_count,
// &self.height_to_empty_address_count,
],
]
.into_iter()
+38 -10
View File
@@ -274,8 +274,6 @@
--default-main-width: 25rem;
--bottom-area: 69vh;
--cube: 50px;
}
[data-resize] {
@@ -1370,41 +1368,71 @@
}
#explorer {
--cube: 4.5rem;
#chain {
display: flex;
flex-direction: column;
gap: calc(var(--cube) * 1.1);
padding: 1rem;
flex-direction: column-reverse;
gap: calc(var(--cube) * 0.66);
padding: 2rem;
.cube {
width: var(--cube);
height: var(--cube);
overflow: hidden;
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
--color: var(--dark-gray);
color: var(--white);
.face {
transform-origin: 0 0;
position: absolute;
width: var(--cube);
height: var(--cube);
padding: 0.1rem;
}
.front {
background-color: var(--orange);
.right {
background-color: var(--color);
transform: rotate(-30deg) skewX(-30deg)
translate(calc(var(--cube) * 1.3), calc(var(--cube) * 1.725))
scaleY(0.864);
}
.top {
background-color: var(--yellow);
background-color: oklch(
from var(--color) calc(l + 0.122) c calc(h + 6)
);
/* background-color: var(--yellow); */
transform: rotate(30deg) skew(-30deg)
translate(calc(var(--cube) * 0.99), calc(var(--cube) * -0.265))
scaleY(0.864);
justify-content: center;
align-items: center;
text-align: center;
display: flex;
font-weight: 900;
text-transform: uppercase;
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
}
.side {
background-color: var(--amber);
.left {
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
background-color: oklch(
from var(--color) calc(l + 0.066) c calc(h + 3)
);
/* background-color: var(--amber); */
transform: rotate(30deg) skewX(30deg)
translate(calc(var(--cube) * 0.3), calc(var(--cube) * 0.6))
scaleY(0.864);
}
.fees {
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
align-items: center;
}
}
}
}
+81 -18
View File
@@ -27,27 +27,90 @@ export function init({
chain.id = "chain";
elements.explorer.append(chain);
chain.append(createCube());
chain.append(createCube());
chain.append(createCube());
chain.append(createCube());
chain.append(createCube());
// vecsResources.getOrCreate(/** @satisfies {Height}*/ (5), "height");
//
const miners = [
{ name: "Foundry USA", color: "orange" },
{ name: "Via BTC", color: "teal" },
{ name: "Ant Pool", color: "emerald" },
{ name: "F2Pool", color: "indigo" },
{ name: "Spider Pool", color: "yellow" },
{ name: "Mara Pool", color: "amber" },
{ name: "SEC Pool", color: "violet" },
{ name: "Luxor", color: "orange" },
{ name: "Brains Pool", color: "cyan" },
];
for (let i = 0; i <= 10; i++) {
const { name, color } = utils.array.random(miners);
const { cubeElement, leftFaceElement, rightFaceElement, topFaceElement } =
createCube();
// cubeElement.style.setProperty("--color", `var(--${color})`);
const heightElement = window.document.createElement("p");
const height = (1_000_002 - i).toString();
const prefixLength = 7 - height.length;
const spanPrefix = window.document.createElement("span");
spanPrefix.style.opacity = "0.5";
spanPrefix.style.userSelect = "none";
heightElement.append(spanPrefix);
spanPrefix.innerHTML = "#" + "0".repeat(prefixLength);
const spanHeight = window.document.createElement("span");
heightElement.append(spanHeight);
spanHeight.innerHTML = height;
rightFaceElement.append(heightElement);
const feesElement = window.document.createElement("div");
feesElement.classList.add("fees");
leftFaceElement.append(feesElement);
const averageFeeElement = window.document.createElement("p");
feesElement.append(averageFeeElement);
averageFeeElement.innerHTML = `~1.41`;
const feeRangeElement = window.document.createElement("p");
feesElement.append(feeRangeElement);
const minFeeElement = window.document.createElement("span");
minFeeElement.innerHTML = `0.11`;
feeRangeElement.append(minFeeElement);
const dashElement = window.document.createElement("span");
dashElement.style.opacity = "0.5";
dashElement.innerHTML = `-`;
feeRangeElement.append(dashElement);
const maxFeeElement = window.document.createElement("span");
maxFeeElement.innerHTML = `12.1`;
feeRangeElement.append(maxFeeElement);
const feeUnitElement = window.document.createElement("p");
feesElement.append(feeUnitElement);
feeUnitElement.style.opacity = "0.5";
feeUnitElement.innerHTML = `sat/vB`;
const spanMiner = window.document.createElement("span");
spanMiner.innerHTML = name;
topFaceElement.append(spanMiner);
chain.prepend(cubeElement);
}
}
function createCube() {
const cubeElement = window.document.createElement("div");
cubeElement.classList.add("cube");
const faceFrontElement = window.document.createElement("div");
faceFrontElement.classList.add("face");
faceFrontElement.classList.add("front");
cubeElement.append(faceFrontElement);
const faceSideElement = window.document.createElement("div");
faceSideElement.classList.add("face");
faceSideElement.classList.add("side");
cubeElement.append(faceSideElement);
const faceTopElement = window.document.createElement("div");
faceTopElement.classList.add("face");
faceTopElement.classList.add("top");
cubeElement.append(faceTopElement);
return cubeElement;
const rightFaceElement = window.document.createElement("div");
rightFaceElement.classList.add("face");
rightFaceElement.classList.add("right");
cubeElement.append(rightFaceElement);
const leftFaceElement = window.document.createElement("div");
leftFaceElement.classList.add("face");
leftFaceElement.classList.add("left");
cubeElement.append(leftFaceElement);
const topFaceElement = window.document.createElement("div");
topFaceElement.classList.add("face");
topFaceElement.classList.add("top");
cubeElement.append(topFaceElement);
return {
cubeElement,
leftFaceElement,
rightFaceElement,
topFaceElement,
};
}
+7
View File
@@ -149,6 +149,13 @@ function createUtils() {
}
return range;
},
/**
* @template T
* @param {T[]} array
*/
random(array) {
return array[Math.floor(Math.random() * array.length)];
},
};
const dom = {
+167 -58
View File
@@ -1300,20 +1300,19 @@ function createPartialOptions({ env, colors }) {
* @property {readonly UTXOGroupObject[]} args.list
*/
/**
* @template {"" | CohortId} T
* @param {T} _key
*/
const fixKey = (_key) =>
_key !== ""
? /** @type {Exclude<"" | `${T}_`, "_">} */ (`${_key}_`)
: /** @type {const} */ ("");
/**
* @param {UTXOGroupObject | UTXOGroupsObject} args
*/
function createUTXOGroupFolder(args) {
/**
* @template {"" | CohortId} T
* @param {T} _key
*/
const fixKey = (_key) =>
_key !== ""
? /** @type {Exclude<"" | `${T}_`, "_">} */ (`${_key}_`)
: /** @type {const} */ ("");
0;
function createCohortGroupFolder(args) {
const list = "list" in args ? args.list : [args];
const useGroupName = "list" in args;
@@ -1474,6 +1473,35 @@ function createPartialOptions({ env, colors }) {
]);
}),
},
// list.filter(({ key }) => key.endsWith("address_count")).map(callbackfn),
// {
// name: "loaded",
// title: `${args.title} Loaded Address Count`,
// bottom: list.flatMap(({ color, name, key: _key }) => {
// const key = fixKey(_key);
// return /** @type {const} */ ([
// createBaseSeries({
// key: `${key}address_count`,
// name: useGroupName ? name : "Loaded",
// color,
// }),
// createBaseSeries({
// key: `${key}empty_address_count`,
// name: useGroupName ? name : "Empty",
// color,
// }),
// ]);
// }),
// },
// {
// name: "empty",
// title: `${args.title} Empty Address Count`,
// bottom: list.flatMap(({ color, name, key: _key }) => {
// const key = fixKey(_key);
// return /** @type {const} */ ([
// ]);
// }),
// },
{
name: "Realized",
tree: [
@@ -2980,9 +3008,9 @@ function createPartialOptions({ env, colors }) {
],
},
{
name: "UTXOs",
name: "Cohorts",
tree: [
createUTXOGroupFolder({
createCohortGroupFolder({
key: "",
name: "",
title: "",
@@ -2991,100 +3019,181 @@ function createPartialOptions({ env, colors }) {
{
name: "term",
tree: [
createUTXOGroupFolder({
createCohortGroupFolder({
name: "Compare",
title: "Compare By Term",
list: terms,
}),
...terms.map(createUTXOGroupFolder),
...terms.map(createCohortGroupFolder),
],
},
{
name: "Up to date",
tree: [
createUTXOGroupFolder({
createCohortGroupFolder({
name: "Compare",
title: "Compare By Up To",
list: upToDate,
}),
...upToDate.map(createUTXOGroupFolder),
...upToDate.map(createCohortGroupFolder),
],
},
{
name: "From Date",
tree: [
createUTXOGroupFolder({
createCohortGroupFolder({
name: "Compare",
title: "Compare By From",
list: fromDate,
}),
...fromDate.map(createUTXOGroupFolder),
...fromDate.map(createCohortGroupFolder),
],
},
{
name: "Date Range",
tree: [
createUTXOGroupFolder({
createCohortGroupFolder({
name: "Compare",
title: "Compare By Range",
list: dateRange,
}),
...dateRange.map(createUTXOGroupFolder),
...dateRange.map(createCohortGroupFolder),
],
},
{
name: "Epoch",
tree: [
createUTXOGroupFolder({
createCohortGroupFolder({
name: "Compare",
title: "Compare By Epoch",
list: epoch,
}),
...epoch.map(createUTXOGroupFolder),
],
},
{
name: "Up to size",
tree: [
createUTXOGroupFolder({
name: "Compare",
title: "Compare By Up To Size",
list: upToSize,
}),
...upToSize.map(createUTXOGroupFolder),
],
},
{
name: "From size",
tree: [
createUTXOGroupFolder({
name: "Compare",
title: "Compare By From Size",
list: fromSize,
}),
...fromSize.map(createUTXOGroupFolder),
],
},
{
name: "Size range",
tree: [
createUTXOGroupFolder({
name: "Compare",
title: "Compare By Size Range",
list: sizeRanges,
}),
...sizeRanges.map(createUTXOGroupFolder),
...epoch.map(createCohortGroupFolder),
],
},
{
name: "type",
tree: [
createUTXOGroupFolder({
createCohortGroupFolder({
name: "Compare",
title: "Compare By Type",
list: type,
}),
...type.map(createUTXOGroupFolder),
...type.map(createCohortGroupFolder),
],
},
{
name: "UTXOs Up to size",
tree: [
createCohortGroupFolder({
name: "Compare",
title: "Compare UTXOs By Up To Size",
list: upToSize,
}),
...upToSize.map(createCohortGroupFolder),
],
},
{
name: "UTXOs From size",
tree: [
createCohortGroupFolder({
name: "Compare",
title: "Compare UTXOs By From Size",
list: fromSize,
}),
...fromSize.map(createCohortGroupFolder),
],
},
{
name: "UTXOs Size range",
tree: [
createCohortGroupFolder({
name: "Compare",
title: "Compare UTXOs By Size Range",
list: sizeRanges,
}),
...sizeRanges.map(createCohortGroupFolder),
],
},
{
name: "Addresses Up to size",
tree: [
createCohortGroupFolder({
name: "Compare",
title: "Compare Addresses By Up To Size",
list: upToSize.map(
(obj) =>
/** @type {const} */ ({
...obj,
key: `addresses_${obj.key}`,
title: `Addresses ${obj.title}`,
}),
),
}),
...upToSize
.map(
(obj) =>
/** @type {const} */ ({
...obj,
key: `addresses_${obj.key}`,
title: `Addresses ${obj.title}`,
}),
)
.map(createCohortGroupFolder),
],
},
{
name: "Addresses From size",
tree: [
createCohortGroupFolder({
name: "Compare",
title: "Compare Addresses By From Size",
list: fromSize.map(
(obj) =>
/** @type {const} */ ({
...obj,
key: `addresses_${obj.key}`,
title: `Addresses ${obj.title}`,
}),
),
}),
...fromSize
.map(
(obj) =>
/** @type {const} */ ({
...obj,
key: `addresses_${obj.key}`,
title: `Addresses ${obj.title}`,
}),
)
.map(createCohortGroupFolder),
],
},
{
name: "Addresses Size range",
tree: [
createCohortGroupFolder({
name: "Compare",
title: "Compare Addresses By Size Range",
list: sizeRanges.map(
(obj) =>
/** @type {const} */ ({
...obj,
key: `addresses_${obj.key}`,
title: `Addresses ${obj.title}`,
}),
),
}),
...sizeRanges
.map(
(obj) =>
/** @type {const} */ ({
...obj,
key: `addresses_${obj.key}`,
title: `Addresses ${obj.title}`,
}),
)
.map(createCohortGroupFolder),
],
},
],
+8 -10
View File
@@ -29,11 +29,11 @@ function createTable({
keyPrefix: "table",
key: "index",
},
}
},
)
);
const index = signals.createMemo(() =>
serializedIndexToIndex(serializedIndex())
serializedIndexToIndex(serializedIndex()),
);
const table = window.document.createElement("table");
@@ -73,7 +73,7 @@ function createTable({
table.append(tbody);
const rowElements = signals.createSignal(
/** @type {HTMLTableRowElement[]} */ ([])
/** @type {HTMLTableRowElement[]} */ ([]),
);
/**
@@ -322,11 +322,11 @@ function createTable({
unit,
});
}
}
},
);
return () => vecId;
}
},
);
});
});
@@ -340,9 +340,7 @@ function createTable({
columns().forEach((vecId, colIndex) => addCol(vecId, colIndex));
obj.addRandomCol = function () {
const vecId =
possibleVecIds[Math.floor(Math.random() * possibleVecIds.length)];
addCol(vecId);
addCol(utils.array.random(possibleVecIds));
};
return () => index;
@@ -393,7 +391,7 @@ export function init({
},
inside: span,
title: "Click or tap to add a column to the table",
})
}),
);
}
@@ -498,7 +496,7 @@ function createIndexToVecIds(vecIdToIndexes) {
});
return arr;
},
/** @type {VecId[][]} */ (Array.from({ length: 24 }))
/** @type {VecId[][]} */ (Array.from({ length: 24 })),
);
indexToVecIds.forEach((arr) => {
arr.sort();