Files
modules/measurement-rate-control/hyper-p2p-sla-budget/index.js
T

163 lines
4.3 KiB
JavaScript

require('bare-process/global')
const EventEmitter = require('bare-events')
const { assertNonEmpty } = require('../../_shared/lib/errors.js')
const PROTOCOL = 'sla-budget/v1'
class HyperP2PSlaBudget extends EventEmitter {
constructor (opts = {}) {
super()
this._services = new Map()
this._stats = { allocated: 0, consumed: 0, rejected: 0, transferred: 0 }
this.defaultCap = opts.defaultCap ?? null
}
allocate (service, tokens) {
assertNonEmpty(service, 'service')
const amount = Number(tokens)
if (!Number.isFinite(amount) || amount < 0) {
throw new Error('tokens must be a non-negative number')
}
const cur = this._services.get(service) || { remaining: 0, total: 0, cap: this.defaultCap }
cur.remaining += amount
cur.total += amount
this._services.set(service, cur)
this._stats.allocated++
this.emit('allocate', { service, tokens: amount, remaining: cur.remaining })
return cur.remaining
}
setCap (service, cap) {
assertNonEmpty(service, 'service')
const n = Number(cap)
if (!Number.isFinite(n) || n < 0) throw new Error('cap must be non-negative')
const cur = this._services.get(service) || { remaining: 0, total: 0, cap: null }
cur.cap = n
if (cur.remaining > n) cur.remaining = n
this._services.set(service, cur)
return cur.cap
}
consume (service, n = 1) {
assertNonEmpty(service, 'service')
const amount = Number(n)
if (!Number.isFinite(amount) || amount < 0) {
throw new Error('n must be a non-negative number')
}
const cur = this._services.get(service)
if (!cur || cur.remaining < amount) {
this._stats.rejected++
this.emit('reject', { service, requested: amount, remaining: cur ? cur.remaining : 0 })
return false
}
cur.remaining -= amount
this._stats.consumed++
this.emit('consume', { service, n: amount, remaining: cur.remaining })
return true
}
transfer (from, to, n) {
assertNonEmpty(from, 'from')
assertNonEmpty(to, 'to')
const amount = Number(n)
if (!Number.isFinite(amount) || amount <= 0) throw new Error('n must be positive')
if (!this.consume(from, amount)) return false
this.allocate(to, amount)
this._stats.transferred++
this.emit('transfer', { from, to, n: amount })
return true
}
remaining (service) {
assertNonEmpty(service, 'service')
const cur = this._services.get(service)
return cur ? cur.remaining : 0
}
canConsume (service, n = 1) {
return this.remaining(service) >= Number(n)
}
usageRatio (service) {
const cur = this._services.get(service)
if (!cur || !cur.total) return 0
return (cur.total - cur.remaining) / cur.total
}
totalRemaining () {
let sum = 0
for (const cur of this._services.values()) sum += cur.remaining
return sum
}
services () {
return [...this._services.keys()].sort()
}
snapshot (service) {
const cur = this._services.get(service)
if (!cur) return null
return {
service,
remaining: cur.remaining,
total: cur.total,
used: cur.total - cur.remaining,
cap: cur.cap,
usageRatio: cur.total ? (cur.total - cur.remaining) / cur.total : 0
}
}
allSnapshots () {
return this.services().map((s) => this.snapshot(s)).filter(Boolean)
}
refill (service, amount) {
return this.allocate(service, amount)
}
reset (service) {
if (service) {
this._services.delete(service)
return true
}
this._services.clear()
return true
}
clearAll () {
return this.reset()
}
consumeBatch (entries) {
if (!Array.isArray(entries)) throw new Error('entries array required')
return entries.map((e) => this.consume(e.service, e.n ?? 1))
}
serviceCount () {
return this._services.size
}
allocateBatch (allocations) {
if (!Array.isArray(allocations)) throw new Error('allocations must be an array')
return allocations.map((a) => this.allocate(a.service, a.amount))
}
getStats () {
return {
...this._stats,
services: this._services.size,
totalRemaining: this.totalRemaining(),
protocol: PROTOCOL
}
}
async ready () { return this }
async close () {
this._services.clear()
this.emit('closed')
}
}
module.exports = { HyperP2PSlaBudget, PROTOCOL }