Add auction withdraw/cancel, marketplace search helpers, update gossip history, autobase fork/lease/indexer/view APIs, link-probe and circuit-loom utilities, trace trees, pheromone ranking, and scheduling/measurement helpers with tests. Co-authored-by: Cursor <[email protected]>
175 lines
5.1 KiB
JavaScript
175 lines
5.1 KiB
JavaScript
require('bare-process/global')
|
|
const EventEmitter = require('bare-events')
|
|
const b4a = require('b4a')
|
|
const { assertNonEmpty } = require('../../_shared/lib/errors.js')
|
|
const { initModuleSwarm, gossipSend } = require('../../_shared/p2p-bare.js')
|
|
|
|
const PROTOCOL = 'auction-gossip/v1'
|
|
|
|
class HyperP2PAuctionGossip extends EventEmitter {
|
|
constructor (opts = {}) {
|
|
super()
|
|
this.topic = opts.topic || null
|
|
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
|
|
this.peerHex = b4a.toString(this.keyPair.publicKey, 'hex')
|
|
this._auctions = new Map()
|
|
this._stats = { opened: 0, bids: 0, closed: 0, gossipIn: 0, gossipOut: 0 }
|
|
this.swarm = null
|
|
this._peerMsgs = null
|
|
}
|
|
|
|
openAuction (auctionId, meta = {}) {
|
|
assertNonEmpty(auctionId, 'auctionId')
|
|
if (this._auctions.has(auctionId)) throw new Error('auction already open')
|
|
const auction = {
|
|
id: auctionId,
|
|
meta,
|
|
status: 'open',
|
|
bids: [],
|
|
openedAt: Date.now(),
|
|
openedBy: this.peerHex
|
|
}
|
|
this._auctions.set(auctionId, auction)
|
|
this._stats.opened++
|
|
this._gossip({ type: 'auction-open', auction })
|
|
this.emit('open', auction)
|
|
return auction
|
|
}
|
|
|
|
placeBid (auctionId, amount, meta = {}) {
|
|
assertNonEmpty(auctionId, 'auctionId')
|
|
if (typeof amount !== 'number' || amount <= 0) throw new Error('amount must be a positive number')
|
|
const auction = this._auctions.get(auctionId)
|
|
if (!auction || auction.status !== 'open') throw new Error('auction not open')
|
|
const bid = {
|
|
amount,
|
|
bidder: this.peerHex,
|
|
meta,
|
|
at: Date.now()
|
|
}
|
|
auction.bids.push(bid)
|
|
auction.bids.sort((a, b) => b.amount - a.amount)
|
|
this._stats.bids++
|
|
this._gossip({ type: 'auction-bid', auctionId, bid })
|
|
this.emit('bid', { auctionId, bid })
|
|
return bid
|
|
}
|
|
|
|
closeAuction (auctionId) {
|
|
const auction = this._auctions.get(auctionId)
|
|
if (!auction) return null
|
|
auction.status = 'closed'
|
|
auction.closedAt = Date.now()
|
|
auction.winner = auction.bids[0] || null
|
|
this._stats.closed++
|
|
this._gossip({ type: 'auction-close', auctionId, winner: auction.winner, closedAt: auction.closedAt })
|
|
this.emit('close', auction)
|
|
return auction
|
|
}
|
|
|
|
getAuction (auctionId) {
|
|
return this._auctions.get(auctionId) || null
|
|
}
|
|
|
|
listOpen () {
|
|
return [...this._auctions.values()].filter((a) => a.status === 'open')
|
|
}
|
|
|
|
listAuctionIds () { return [...this._auctions.keys()] }
|
|
|
|
highestBid (auctionId) {
|
|
const a = this._auctions.get(auctionId)
|
|
return a && a.bids.length ? a.bids[0] : null
|
|
}
|
|
|
|
withdrawBid (auctionId, bidder = null) {
|
|
const a = this._auctions.get(auctionId)
|
|
if (!a || a.status !== 'open') return 0
|
|
const who = bidder || this.peerHex
|
|
const before = a.bids.length
|
|
a.bids = a.bids.filter((b) => b.bidder !== who)
|
|
a.bids.sort((x, y) => y.amount - x.amount)
|
|
const removed = before - a.bids.length
|
|
if (removed) {
|
|
this._gossip({ type: 'auction-withdraw', auctionId, bidder: who })
|
|
this.emit('withdraw', { auctionId, bidder: who, removed })
|
|
}
|
|
return removed
|
|
}
|
|
|
|
cancelAuction (auctionId) {
|
|
const a = this._auctions.get(auctionId)
|
|
if (!a || a.status !== 'open') return false
|
|
this._auctions.delete(auctionId)
|
|
this._gossip({ type: 'auction-cancel', auctionId })
|
|
this.emit('cancel', { auctionId })
|
|
return true
|
|
}
|
|
|
|
_gossip (payload) {
|
|
if (!this._peerMsgs) return
|
|
gossipSend(this, payload)
|
|
this._stats.gossipOut++
|
|
}
|
|
|
|
_onGossip (data) {
|
|
if (!data || !data.type) return
|
|
this._stats.gossipIn++
|
|
if (data.type === 'auction-open' && data.auction) {
|
|
this._auctions.set(data.auction.id, data.auction)
|
|
this.emit('remote-open', data.auction)
|
|
}
|
|
if (data.type === 'auction-bid' && data.auctionId && data.bid) {
|
|
const a = this._auctions.get(data.auctionId)
|
|
if (a && a.status === 'open') {
|
|
a.bids.push(data.bid)
|
|
a.bids.sort((x, y) => y.amount - x.amount)
|
|
this.emit('remote-bid', data)
|
|
}
|
|
}
|
|
if (data.type === 'auction-close' && data.auctionId) {
|
|
const a = this._auctions.get(data.auctionId)
|
|
if (a) {
|
|
a.status = 'closed'
|
|
a.winner = data.winner
|
|
a.closedAt = data.closedAt
|
|
this.emit('remote-close', a)
|
|
}
|
|
}
|
|
if (data.type === 'auction-withdraw' && data.auctionId) {
|
|
this.withdrawBid(data.auctionId, data.bidder)
|
|
}
|
|
if (data.type === 'auction-cancel' && data.auctionId) {
|
|
this._auctions.delete(data.auctionId)
|
|
}
|
|
}
|
|
|
|
getStats () {
|
|
return {
|
|
...this._stats,
|
|
auctions: this._auctions.size,
|
|
open: this.listOpen().length,
|
|
protocol: PROTOCOL
|
|
}
|
|
}
|
|
|
|
async ready () {
|
|
if (this.swarm || !this.topic) return this
|
|
await initModuleSwarm(this, {
|
|
keyPair: this.keyPair,
|
|
topic: this.topic,
|
|
protocol: PROTOCOL,
|
|
onmessage: (d) => this._onGossip(d)
|
|
})
|
|
return this
|
|
}
|
|
|
|
async close () {
|
|
if (this.swarm) await this.swarm.destroy().catch(() => {})
|
|
this.swarm = null
|
|
this._auctions.clear()
|
|
}
|
|
}
|
|
|
|
module.exports = { HyperP2PAuctionGossip, PROTOCOL }
|