mirror of
https://github.com/Retropex/bitfeed.git
synced 2025-05-13 03:30:47 +02:00
318 lines
10 KiB
JavaScript
318 lines
10 KiB
JavaScript
import TxPoolScene from '../models/TxPoolScene.js'
|
|
import TxMondrianPoolScene from '../models/TxMondrianPoolScene.js'
|
|
import TxBlockScene from '../models/TxBlockScene.js'
|
|
import BitcoinTx from '../models/BitcoinTx.js'
|
|
import BitcoinBlock from '../models/BitcoinBlock.js'
|
|
import TxSprite from '../models/TxSprite.js'
|
|
import { FastVertexArray } from '../utils/memory.js'
|
|
import { txQueueLength, txCount, mempoolCount, mempoolScreenHeight, blockVisible, currentBlock, selectedTx, blockAreaSize, highlight } from '../stores.js'
|
|
import config from "../config.js"
|
|
|
|
export default class TxController {
|
|
constructor ({ width, height }) {
|
|
this.vertexArray = new FastVertexArray(2048, TxSprite.dataSize, txCount)
|
|
this.debugVertexArray = new FastVertexArray(1024, TxSprite.dataSize)
|
|
this.txs = {}
|
|
this.expiredTxs = {}
|
|
this.poolScene = new TxMondrianPoolScene({ width, height, controller: this, heightStore: mempoolScreenHeight })
|
|
this.blockAreaSize = Math.min(window.innerWidth * 0.75, window.innerHeight / 2.5)
|
|
blockAreaSize.set(this.blockAreaSize)
|
|
this.blockScene = null
|
|
this.clearBlockTimeout = null
|
|
this.txDelay = 0 //config.txDelay
|
|
this.maxTxDelay = config.txDelay
|
|
this.knownBlocks = {}
|
|
|
|
this.selectedTx = null
|
|
this.selectionLocked = false
|
|
|
|
this.pendingTxs = []
|
|
this.pendingMap = {}
|
|
this.queueTimeout = null
|
|
this.queueLength = 0
|
|
|
|
highlight.subscribe(criteria => {
|
|
this.highlightCriteria = criteria
|
|
this.applyHighlighting()
|
|
})
|
|
|
|
this.scheduleQueue(1000)
|
|
}
|
|
|
|
getVertexData () {
|
|
return this.vertexArray.getVertexData()
|
|
}
|
|
|
|
getDebugVertexData () {
|
|
return this.debugVertexArray.getVertexData()
|
|
}
|
|
|
|
getScenes () {
|
|
if (this.blockScene) return [this.poolScene, this.blockScene]
|
|
else return [this.poolScene]
|
|
}
|
|
|
|
redoLayout ({ width, height }) {
|
|
this.poolScene.layoutAll({ width, height })
|
|
if (this.blockScene) {
|
|
this.blockScene.layoutAll({ width: this.blockAreaSize, height: this.blockAreaSize })
|
|
}
|
|
}
|
|
|
|
resize ({ width, height }) {
|
|
this.blockAreaSize = Math.min(window.innerWidth * 0.75, window.innerHeight / 2.5)
|
|
blockAreaSize.set(this.blockAreaSize)
|
|
this.redoLayout({ width, height })
|
|
}
|
|
|
|
applyHighlighting () {
|
|
this.poolScene.applyHighlighting(this.highlightCriteria)
|
|
if (this.blockScene) {
|
|
this.blockScene.applyHighlighting(this.highlightCriteria)
|
|
}
|
|
}
|
|
|
|
addTx (txData) {
|
|
const tx = new BitcoinTx(txData, this.vertexArray)
|
|
tx.applyHighlighting(this.highlightCriteria)
|
|
if (!this.txs[tx.id] && !this.expiredTxs[tx.id]) {
|
|
this.pendingTxs.push([tx, performance.now()])
|
|
this.pendingTxs[tx.id] = tx
|
|
txQueueLength.increment()
|
|
}
|
|
}
|
|
|
|
// Dual-strategy queue processing:
|
|
// - ensure transactions are queued for at least txDelay
|
|
// - when queue length exceeds 500, process iteratively to avoid unbounded growth
|
|
// - while queue is small, use jittered timeouts to evenly distribute arrivals
|
|
//
|
|
// transactions tend to arrive in groups, so for smoothest
|
|
// animation the queue should stay short but never empty.
|
|
processQueue () {
|
|
let done
|
|
let delay
|
|
while (!done) {
|
|
if (this.pendingTxs && this.pendingTxs.length) {
|
|
if (this.txs[this.pendingTxs[0][0].id] || this.expiredTxs[this.pendingTxs[0][0].id]) {
|
|
// duplicate transaction, skip without delay
|
|
const tx = this.pendingTxs.shift()[0]
|
|
delete this.pendingMap[tx.id]
|
|
txQueueLength.decrement()
|
|
} else {
|
|
const timeSince = performance.now() - this.pendingTxs[0][1]
|
|
if (timeSince > this.txDelay) {
|
|
//process the next tx in the queue, if it arrived longer ago than txDelay
|
|
if (this.txDelay < this.maxTxDelay) {
|
|
// slowly ramp up from 0 to maxTxDelay on start, so there's no wait for the first txs on page load
|
|
this.txDelay += 50
|
|
}
|
|
const tx = this.pendingTxs.shift()[0]
|
|
delete this.pendingMap[tx.id]
|
|
txQueueLength.decrement()
|
|
this.txs[tx.id] = tx
|
|
this.poolScene.insert(this.txs[tx.id])
|
|
mempoolCount.increment()
|
|
} else {
|
|
// end the loop when the head of the queue arrived more recently than txDelay
|
|
done = true
|
|
// schedule to continue processing when head of queue matures
|
|
delay = this.txDelay - timeSince
|
|
}
|
|
if (this.pendingTxs.length < 500) {
|
|
// or the queue is under 500
|
|
done = true
|
|
}
|
|
// otherwise keep processing the queue
|
|
}
|
|
} else done = true
|
|
}
|
|
// randomly jitter arrival times so that txs enter more naturally
|
|
// with jittered delay inversely proportional to size of queue
|
|
let jitter = Math.random() * Math.max(1, (500 - this.pendingTxs.length))
|
|
this.scheduleQueue(delay || jitter)
|
|
}
|
|
|
|
scheduleQueue (delay) {
|
|
if (this.queueTimeout) clearTimeout(this.queueTimeout)
|
|
this.queueTimeout = setTimeout(() => {
|
|
this.processQueue()
|
|
}, delay)
|
|
}
|
|
|
|
addBlock (blockData) {
|
|
// discard duplicate blocks
|
|
if (!blockData || !blockData.id || this.knownBlocks[blockData.id]) {
|
|
return
|
|
}
|
|
|
|
this.poolScene.scrollLock = true
|
|
|
|
const block = (blockData && blockData.isBlock) ? blockData : new BitcoinBlock(blockData)
|
|
this.knownBlocks[block.id] = true
|
|
if (this.clearBlockTimeout) clearTimeout(this.clearBlockTimeout)
|
|
|
|
this.expiredTxs = {}
|
|
|
|
this.clearBlock()
|
|
|
|
this.blockScene = new TxBlockScene({ width: this.blockAreaSize, height: this.blockAreaSize, blockId: block.id, controller: this })
|
|
let poolCount = 0
|
|
let knownCount = 0
|
|
let unknownCount = 0
|
|
for (let i = 0; i < block.txns.length; i++) {
|
|
if (this.poolScene.remove(block.txns[i].id)) {
|
|
poolCount++
|
|
knownCount++
|
|
this.txs[block.txns[i].id].setBlock(block.id)
|
|
this.blockScene.insert(this.txs[block.txns[i].id], false)
|
|
} else if (this.pendingMap[block.txns[i].id]) {
|
|
knownCount++
|
|
const tx = this.pendingMap[tx.id]
|
|
const pendingIndex = this.pendingTxs.indexOf(tx)
|
|
if (pendingIndex >= 0) this.pendingTxs.splice(pendingIndex, 1)
|
|
delete this.pendingMap[tx.id]
|
|
tx.setBlock(block.id)
|
|
this.txs[tx.id] = tx
|
|
this.blockScene.insert(tx, false)
|
|
} else {
|
|
unknownCount++
|
|
const tx = new BitcoinTx({
|
|
...block.txns[i],
|
|
block: block.id
|
|
}, this.vertexArray)
|
|
this.txs[tx.id] = tx
|
|
this.txs[tx.id].applyHighlighting(this.highlightCriteria)
|
|
this.blockScene.insert(tx, false)
|
|
}
|
|
this.expiredTxs[block.txns[i].id] = true
|
|
}
|
|
console.log(`New block with ${knownCount} known transactions and ${unknownCount} unknown transactions`)
|
|
mempoolCount.subtract(poolCount)
|
|
this.blockScene.initialLayout()
|
|
setTimeout(() => { this.poolScene.scrollLock = false; this.poolScene.layoutAll() }, 4000)
|
|
|
|
currentBlock.set(block)
|
|
blockVisible.set(true)
|
|
|
|
return block
|
|
}
|
|
|
|
simulateBlock () {
|
|
for (var i = 0; i < 1000; i++) {
|
|
this.addTx(new BitcoinTx({
|
|
version: 'simulated',
|
|
time: Date.now(),
|
|
id: `simulated_${i}_${Math.random()}`,
|
|
value: Math.floor(Math.pow(2,(0.1-(Math.log(1 - Math.random()))) * 8))
|
|
}))
|
|
}
|
|
const simulatedTxns = this.pendingTxs.map(pending => {
|
|
return {
|
|
version: pending[0].version,
|
|
id: pending[0].id,
|
|
time: pending[0].time,
|
|
value: pending[0].value
|
|
}
|
|
})
|
|
// const simulatedTxns = []
|
|
Object.values(this.poolScene.txs).forEach(tx => {
|
|
if (Math.random() < 0.5) {
|
|
simulatedTxns.push({
|
|
version: tx.version,
|
|
id: tx.id,
|
|
time: tx.time,
|
|
value: tx.value
|
|
})
|
|
}
|
|
})
|
|
const block = new BitcoinBlock({
|
|
version: 'fake',
|
|
id: 'fake_block' + Math.random(),
|
|
value: 12345678900,
|
|
prev_block: 'also_fake',
|
|
merkle_root: 'merkle',
|
|
timestamp: Date.now() / 1000,
|
|
bits: 'none',
|
|
bytes: 1379334,
|
|
txn_count: simulatedTxns.length,
|
|
txns: simulatedTxns
|
|
})
|
|
setTimeout(() => {
|
|
this.addBlock(block)
|
|
}, 2500)
|
|
return block
|
|
}
|
|
|
|
simulateDumpTx (n, value) {
|
|
for (var i = 0; i < n; i++) {
|
|
this.addTx(new BitcoinTx({
|
|
version: 'simulated',
|
|
time: Date.now(),
|
|
id: `simulated_${i}_${Math.random()}`,
|
|
value: value || Math.min(10000, Math.floor(Math.pow(2,(0.1-(Math.log(1 - Math.random()))) * 8))) // horrible but plausible distribution of tx values
|
|
// value: value || Math.pow(10,Math.floor(Math.random() * 5))
|
|
}, this.vertexArray))
|
|
}
|
|
}
|
|
|
|
hideBlock () {
|
|
if (this.blockScene) {
|
|
this.blockScene.hide()
|
|
}
|
|
}
|
|
|
|
showBlock () {
|
|
if (this.blockScene) {
|
|
this.blockScene.show()
|
|
}
|
|
}
|
|
|
|
clearBlock () {
|
|
if (this.blockScene) {
|
|
this.blockScene.expire()
|
|
}
|
|
currentBlock.set(null)
|
|
if (this.blockVisibleUnsub) this.blockVisibleUnsub()
|
|
}
|
|
|
|
destroyTx (id) {
|
|
this.getScenes().forEach(scene => {
|
|
scene.remove(id)
|
|
})
|
|
if (this.txs[id]) this.txs[id].destroy()
|
|
delete this.txs[id]
|
|
}
|
|
|
|
mouseMove (position) {
|
|
if (this.poolScene && !this.selectionLocked) {
|
|
let selected = this.poolScene.selectAt(position)
|
|
if (!selected && this.blockScene && !this.blockScene.hidden) selected = this.blockScene.selectAt(position)
|
|
|
|
if (selected !== this.selectedTx) {
|
|
if (this.selectedTx) this.selectedTx.hoverOff()
|
|
if (selected) selected.hoverOn()
|
|
}
|
|
this.selectedTx = selected
|
|
selectedTx.set(this.selectedTx)
|
|
}
|
|
}
|
|
|
|
mouseClick (position) {
|
|
if (this.poolScene) {
|
|
let selected = this.poolScene.selectAt(position)
|
|
if (!selected && this.blockScene && !this.blockScene.hidden) selected = this.blockScene.selectAt(position)
|
|
|
|
let sameTx = true
|
|
if (selected !== this.selectedTx) {
|
|
sameTx = false
|
|
if (this.selectedTx) this.selectedTx.hoverOff()
|
|
if (selected) selected.hoverOn()
|
|
}
|
|
this.selectedTx = selected
|
|
selectedTx.set(this.selectedTx)
|
|
this.selectionLocked = !!this.selectedTx && !(this.selectionLocked && sameTx)
|
|
}
|
|
}
|
|
}
|