@relay-federation/bridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,172 @@
1
+ import { EventEmitter } from 'node:events'
2
+
3
+ const DEFAULT_GRACE_PERIOD_MS = 24 * 60 * 60 * 1000 // 24 hours
4
+ const DEFAULT_INACTIVE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
5
+
6
+ /**
7
+ * Peer health tracker — monitors peer connectivity and applies
8
+ * grace period logic + auto-deregistration detection.
9
+ *
10
+ * Rules:
11
+ * - Tracks last-seen timestamp for each peer
12
+ * - Grace period (24h): short outages don't count against peer score
13
+ * - Inactive threshold (7d): peers unreachable for 7+ days are flagged inactive
14
+ * - Inactive peers should be excluded from peer lists locally
15
+ *
16
+ * Emits:
17
+ * 'peer:grace' — { pubkeyHex, offlineSince } — peer entered grace period
18
+ * 'peer:inactive' — { pubkeyHex, offlineSince } — peer flagged as inactive (7+ days offline)
19
+ * 'peer:recovered' — { pubkeyHex } — peer came back online
20
+ */
21
+ export class PeerHealth extends EventEmitter {
22
+ /**
23
+ * @param {object} [opts]
24
+ * @param {number} [opts.gracePeriodMs=86400000] — 24 hours
25
+ * @param {number} [opts.inactiveThresholdMs=604800000] — 7 days
26
+ */
27
+ constructor (opts = {}) {
28
+ super()
29
+ this._gracePeriodMs = opts.gracePeriodMs ?? DEFAULT_GRACE_PERIOD_MS
30
+ this._inactiveThresholdMs = opts.inactiveThresholdMs ?? DEFAULT_INACTIVE_THRESHOLD_MS
31
+
32
+ /** @type {Map<string, { lastSeen: number, offlineSince: number|null, status: string }>} */
33
+ this._peers = new Map()
34
+ }
35
+
36
+ /**
37
+ * Record that we successfully communicated with a peer.
38
+ * Resets offline tracking.
39
+ * @param {string} pubkeyHex
40
+ */
41
+ recordSeen (pubkeyHex) {
42
+ const prev = this._peers.get(pubkeyHex)
43
+ const wasOffline = prev && prev.offlineSince !== null
44
+
45
+ this._peers.set(pubkeyHex, {
46
+ lastSeen: Date.now(),
47
+ offlineSince: null,
48
+ status: 'online'
49
+ })
50
+
51
+ if (wasOffline) {
52
+ this.emit('peer:recovered', { pubkeyHex })
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Record that a peer has disconnected or failed to respond.
58
+ * Starts the offline timer if not already started.
59
+ * @param {string} pubkeyHex
60
+ */
61
+ recordOffline (pubkeyHex) {
62
+ const existing = this._peers.get(pubkeyHex)
63
+
64
+ if (existing && existing.offlineSince !== null) {
65
+ // Already tracking offline — don't reset the timer
66
+ return
67
+ }
68
+
69
+ this._peers.set(pubkeyHex, {
70
+ lastSeen: existing ? existing.lastSeen : 0,
71
+ offlineSince: Date.now(),
72
+ status: 'offline'
73
+ })
74
+ }
75
+
76
+ /**
77
+ * Check the health status of a peer.
78
+ *
79
+ * @param {string} pubkeyHex
80
+ * @returns {'online'|'grace'|'inactive'|'unknown'}
81
+ */
82
+ getStatus (pubkeyHex) {
83
+ const peer = this._peers.get(pubkeyHex)
84
+ if (!peer) return 'unknown'
85
+ if (peer.offlineSince === null) return 'online'
86
+
87
+ const offlineDuration = Date.now() - peer.offlineSince
88
+
89
+ if (offlineDuration >= this._inactiveThresholdMs) {
90
+ return 'inactive'
91
+ }
92
+
93
+ if (offlineDuration >= this._gracePeriodMs) {
94
+ return 'offline'
95
+ }
96
+
97
+ return 'grace'
98
+ }
99
+
100
+ /**
101
+ * Check all peers and emit events for status changes.
102
+ * Call this periodically (e.g. every 10 minutes).
103
+ *
104
+ * @returns {{ grace: string[], inactive: string[] }} Lists of pubkeys in each state
105
+ */
106
+ checkAll () {
107
+ const grace = []
108
+ const inactive = []
109
+
110
+ for (const [pubkeyHex, peer] of this._peers) {
111
+ if (peer.offlineSince === null) continue
112
+
113
+ const status = this.getStatus(pubkeyHex)
114
+ const prevStatus = peer.status
115
+
116
+ if (status === 'inactive' && prevStatus !== 'inactive') {
117
+ peer.status = 'inactive'
118
+ inactive.push(pubkeyHex)
119
+ this.emit('peer:inactive', { pubkeyHex, offlineSince: peer.offlineSince })
120
+ } else if (status === 'grace' && prevStatus !== 'grace') {
121
+ peer.status = 'grace'
122
+ grace.push(pubkeyHex)
123
+ this.emit('peer:grace', { pubkeyHex, offlineSince: peer.offlineSince })
124
+ }
125
+ }
126
+
127
+ return { grace, inactive }
128
+ }
129
+
130
+ /**
131
+ * Get the last-seen timestamp for a peer.
132
+ * @param {string} pubkeyHex
133
+ * @returns {number|null} Unix timestamp in ms, or null if unknown
134
+ */
135
+ getLastSeen (pubkeyHex) {
136
+ const peer = this._peers.get(pubkeyHex)
137
+ return peer ? peer.lastSeen : null
138
+ }
139
+
140
+ /**
141
+ * Get all peers flagged as inactive (7+ days offline).
142
+ * @returns {string[]} Array of pubkeyHex strings
143
+ */
144
+ getInactivePeers () {
145
+ const result = []
146
+ for (const [pubkeyHex] of this._peers) {
147
+ if (this.getStatus(pubkeyHex) === 'inactive') {
148
+ result.push(pubkeyHex)
149
+ }
150
+ }
151
+ return result
152
+ }
153
+
154
+ /**
155
+ * Check if scoring impact should be suppressed (grace period active).
156
+ * During grace period, bad pings should NOT count against the peer.
157
+ *
158
+ * @param {string} pubkeyHex
159
+ * @returns {boolean} true if peer is in grace period (suppress scoring)
160
+ */
161
+ isInGracePeriod (pubkeyHex) {
162
+ return this.getStatus(pubkeyHex) === 'grace'
163
+ }
164
+
165
+ /**
166
+ * Remove a peer from health tracking.
167
+ * @param {string} pubkeyHex
168
+ */
169
+ removePeer (pubkeyHex) {
170
+ this._peers.delete(pubkeyHex)
171
+ }
172
+ }
@@ -0,0 +1,214 @@
1
+ import { EventEmitter } from 'node:events'
2
+ import { WebSocketServer } from 'ws'
3
+ import { PeerConnection } from './peer-connection.js'
4
+
5
+ /**
6
+ * PeerManager — manages multiple peer connections.
7
+ *
8
+ * Handles:
9
+ * - Outbound connections to discovered peers
10
+ * - Inbound connections from other bridges (via WSS server)
11
+ * - Broadcasting messages to all connected peers
12
+ * - Peer lifecycle (connect, disconnect, reconnect)
13
+ *
14
+ * Events:
15
+ * 'peer:connect' — { pubkeyHex, endpoint }
16
+ * 'peer:disconnect' — { pubkeyHex, endpoint }
17
+ * 'peer:message' — { pubkeyHex, message }
18
+ * 'peer:error' — { pubkeyHex, error }
19
+ */
20
+ export class PeerManager extends EventEmitter {
21
+ /**
22
+ * @param {object} [opts]
23
+ * @param {number} [opts.maxPeers=20] - Maximum number of peer connections
24
+ */
25
+ constructor (opts = {}) {
26
+ super()
27
+ this.maxPeers = opts.maxPeers || 20
28
+ /** @type {Map<string, PeerConnection>} pubkeyHex → PeerConnection */
29
+ this.peers = new Map()
30
+ this._server = null
31
+ }
32
+
33
+ /**
34
+ * Connect to a discovered peer (outbound).
35
+ *
36
+ * @param {object} peer - Peer from buildPeerList()
37
+ * @param {string} peer.pubkeyHex
38
+ * @param {string} peer.endpoint
39
+ * @returns {PeerConnection|null} The connection, or null if at capacity
40
+ */
41
+ connectToPeer (peer) {
42
+ if (this.peers.has(peer.pubkeyHex)) {
43
+ return this.peers.get(peer.pubkeyHex)
44
+ }
45
+
46
+ if (this.peers.size >= this.maxPeers) {
47
+ return null
48
+ }
49
+
50
+ const conn = new PeerConnection({
51
+ endpoint: peer.endpoint,
52
+ pubkeyHex: peer.pubkeyHex
53
+ })
54
+
55
+ this._attachPeerEvents(conn)
56
+ this.peers.set(peer.pubkeyHex, conn)
57
+ conn.connect()
58
+
59
+ return conn
60
+ }
61
+
62
+ /**
63
+ * Accept an inbound peer connection.
64
+ *
65
+ * @param {WebSocket} socket - Incoming WebSocket
66
+ * @param {string} pubkeyHex - Peer's pubkey (from handshake)
67
+ * @param {string} endpoint - Peer's advertised endpoint
68
+ * @returns {PeerConnection|null}
69
+ */
70
+ acceptPeer (socket, pubkeyHex, endpoint) {
71
+ if (this.peers.has(pubkeyHex)) {
72
+ // Already connected — close the duplicate
73
+ socket.close()
74
+ return this.peers.get(pubkeyHex)
75
+ }
76
+
77
+ if (this.peers.size >= this.maxPeers) {
78
+ socket.close()
79
+ return null
80
+ }
81
+
82
+ const conn = new PeerConnection({
83
+ endpoint,
84
+ pubkeyHex,
85
+ socket
86
+ })
87
+
88
+ this._attachPeerEvents(conn)
89
+ this.peers.set(pubkeyHex, conn)
90
+
91
+ return conn
92
+ }
93
+
94
+ /**
95
+ * Disconnect a specific peer.
96
+ *
97
+ * @param {string} pubkeyHex
98
+ */
99
+ disconnectPeer (pubkeyHex) {
100
+ const conn = this.peers.get(pubkeyHex)
101
+ if (conn) {
102
+ conn.destroy()
103
+ this.peers.delete(pubkeyHex)
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Broadcast a message to all connected peers.
109
+ *
110
+ * @param {object} msg - JSON message with `type` field
111
+ * @param {string} [excludePubkey] - Optional pubkey to exclude (e.g. message source)
112
+ * @returns {number} Number of peers the message was sent to
113
+ */
114
+ broadcast (msg, excludePubkey) {
115
+ let sent = 0
116
+ for (const [pubkeyHex, conn] of this.peers) {
117
+ if (pubkeyHex === excludePubkey) continue
118
+ if (conn.send(msg)) sent++
119
+ }
120
+ return sent
121
+ }
122
+
123
+ /**
124
+ * Get count of currently connected peers.
125
+ * @returns {number}
126
+ */
127
+ connectedCount () {
128
+ let count = 0
129
+ for (const conn of this.peers.values()) {
130
+ if (conn.connected) count++
131
+ }
132
+ return count
133
+ }
134
+
135
+ /**
136
+ * Start a WebSocket server for inbound connections.
137
+ *
138
+ * @param {object} opts
139
+ * @param {number} opts.port - Port to listen on
140
+ * @param {string} [opts.host='0.0.0.0'] - Host to bind to
141
+ * @returns {Promise<void>}
142
+ */
143
+ startServer (opts) {
144
+ return new Promise((resolve, reject) => {
145
+ this._server = new WebSocketServer({
146
+ port: opts.port,
147
+ host: opts.host || '0.0.0.0'
148
+ })
149
+
150
+ this._server.on('listening', () => resolve())
151
+ this._server.on('error', (err) => reject(err))
152
+
153
+ this._server.on('connection', (ws) => {
154
+ // Inbound connections need to identify themselves via handshake.
155
+ // For now, we hold the socket and wait for a 'hello' message.
156
+ const timeout = setTimeout(() => {
157
+ ws.close() // No hello within 10 seconds
158
+ }, 10000)
159
+
160
+ ws.once('message', (data) => {
161
+ clearTimeout(timeout)
162
+ try {
163
+ const msg = JSON.parse(data.toString())
164
+ if (msg.type === 'hello' && msg.pubkey && msg.endpoint) {
165
+ const conn = this.acceptPeer(ws, msg.pubkey, msg.endpoint)
166
+ if (conn) {
167
+ this.emit('peer:connect', { pubkeyHex: msg.pubkey, endpoint: msg.endpoint })
168
+ // Forward the hello as a regular message too
169
+ conn.emit('message', msg)
170
+ }
171
+ } else {
172
+ ws.close() // Invalid hello
173
+ }
174
+ } catch {
175
+ ws.close() // Invalid JSON
176
+ }
177
+ })
178
+ })
179
+ })
180
+ }
181
+
182
+ /**
183
+ * Stop the WebSocket server and disconnect all peers.
184
+ */
185
+ async shutdown () {
186
+ for (const [pubkeyHex, conn] of this.peers) {
187
+ conn.destroy()
188
+ }
189
+ this.peers.clear()
190
+
191
+ if (this._server) {
192
+ await new Promise(resolve => this._server.close(resolve))
193
+ this._server = null
194
+ }
195
+ }
196
+
197
+ _attachPeerEvents (conn) {
198
+ conn.on('open', () => {
199
+ this.emit('peer:connect', { pubkeyHex: conn.pubkeyHex, endpoint: conn.endpoint })
200
+ })
201
+
202
+ conn.on('message', (msg) => {
203
+ this.emit('peer:message', { pubkeyHex: conn.pubkeyHex, message: msg })
204
+ })
205
+
206
+ conn.on('close', () => {
207
+ this.emit('peer:disconnect', { pubkeyHex: conn.pubkeyHex, endpoint: conn.endpoint })
208
+ })
209
+
210
+ conn.on('error', (err) => {
211
+ this.emit('peer:error', { pubkeyHex: conn.pubkeyHex, error: err })
212
+ })
213
+ }
214
+ }
@@ -0,0 +1,285 @@
1
+ import { EventEmitter } from 'node:events'
2
+
3
+ const WEIGHTS = {
4
+ uptime: 0.3,
5
+ responseTime: 0.2,
6
+ dataAccuracy: 0.4,
7
+ stakeAge: 0.1
8
+ }
9
+
10
+ // Response time normalization: 1.0 = <100ms, 0.0 = >5000ms
11
+ const RT_FLOOR = 100
12
+ const RT_CEIL = 5000
13
+
14
+ // Stake age normalization: log2(days) / 10, capped at 1.0
15
+ const STAKE_AGE_DIVISOR = 10
16
+
17
+ // Rolling windows
18
+ const DEFAULT_ACCURACY_WINDOW = 1000
19
+ const DEFAULT_UPTIME_WINDOW = 1000 // ~7 days at 10-min ping intervals
20
+
21
+ /**
22
+ * Per-peer metrics bucket.
23
+ */
24
+ function createMetrics () {
25
+ return {
26
+ // Uptime tracking
27
+ pings: 0,
28
+ pongs: 0,
29
+
30
+ // Response time — rolling average
31
+ latencies: [],
32
+
33
+ // Data accuracy — rolling window of booleans (true = good)
34
+ accuracyLog: [],
35
+
36
+ // Stake age in days
37
+ stakeAgeDays: 0
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Peer scoring engine.
43
+ *
44
+ * Computes local reputation scores for each connected peer.
45
+ * Formula: 0.3 * uptime + 0.2 * response_time + 0.4 * data_accuracy + 0.1 * stake_age
46
+ *
47
+ * Emits:
48
+ * 'score:update' — { pubkeyHex, score, metrics }
49
+ */
50
+ export class PeerScorer extends EventEmitter {
51
+ /**
52
+ * @param {object} [opts]
53
+ * @param {number} [opts.accuracyWindow=1000] — rolling window size for data accuracy
54
+ * @param {number} [opts.uptimeWindow=1000] — rolling window size for uptime pings
55
+ * @param {number} [opts.latencyWindow=100] — rolling window size for latency samples
56
+ */
57
+ constructor (opts = {}) {
58
+ super()
59
+ this._peers = new Map() // pubkeyHex → metrics
60
+ this._accuracyWindow = opts.accuracyWindow || DEFAULT_ACCURACY_WINDOW
61
+ this._uptimeWindow = opts.uptimeWindow || DEFAULT_UPTIME_WINDOW
62
+ this._latencyWindow = opts.latencyWindow || 100
63
+ }
64
+
65
+ /**
66
+ * Get or create metrics for a peer.
67
+ * @param {string} pubkeyHex
68
+ * @returns {object}
69
+ */
70
+ _getMetrics (pubkeyHex) {
71
+ if (!this._peers.has(pubkeyHex)) {
72
+ this._peers.set(pubkeyHex, createMetrics())
73
+ }
74
+ return this._peers.get(pubkeyHex)
75
+ }
76
+
77
+ /**
78
+ * Record a successful ping response.
79
+ * @param {string} pubkeyHex
80
+ * @param {number} latencyMs — round-trip time in milliseconds
81
+ */
82
+ recordPing (pubkeyHex, latencyMs) {
83
+ const m = this._getMetrics(pubkeyHex)
84
+ m.pings++
85
+ m.pongs++
86
+
87
+ // Trim uptime window
88
+ if (m.pings > this._uptimeWindow) {
89
+ // Approximate: scale down proportionally
90
+ const ratio = m.pongs / m.pings
91
+ m.pings = this._uptimeWindow
92
+ m.pongs = Math.round(ratio * this._uptimeWindow)
93
+ }
94
+
95
+ // Record latency
96
+ m.latencies.push(latencyMs)
97
+ if (m.latencies.length > this._latencyWindow) {
98
+ m.latencies.shift()
99
+ }
100
+
101
+ this._emitUpdate(pubkeyHex)
102
+ }
103
+
104
+ /**
105
+ * Record a ping timeout (no response).
106
+ * @param {string} pubkeyHex
107
+ */
108
+ recordPingTimeout (pubkeyHex) {
109
+ const m = this._getMetrics(pubkeyHex)
110
+ m.pings++
111
+
112
+ // Trim uptime window
113
+ if (m.pings > this._uptimeWindow) {
114
+ const ratio = m.pongs / m.pings
115
+ m.pings = this._uptimeWindow
116
+ m.pongs = Math.round(ratio * this._uptimeWindow)
117
+ }
118
+
119
+ this._emitUpdate(pubkeyHex)
120
+ }
121
+
122
+ /**
123
+ * Record a valid data relay (good header or tx).
124
+ * @param {string} pubkeyHex
125
+ */
126
+ recordGoodData (pubkeyHex) {
127
+ const m = this._getMetrics(pubkeyHex)
128
+ m.accuracyLog.push(true)
129
+ if (m.accuracyLog.length > this._accuracyWindow) {
130
+ m.accuracyLog.shift()
131
+ }
132
+ this._emitUpdate(pubkeyHex)
133
+ }
134
+
135
+ /**
136
+ * Record an invalid data relay (bad header or tx).
137
+ * @param {string} pubkeyHex
138
+ */
139
+ recordBadData (pubkeyHex) {
140
+ const m = this._getMetrics(pubkeyHex)
141
+ m.accuracyLog.push(false)
142
+ if (m.accuracyLog.length > this._accuracyWindow) {
143
+ m.accuracyLog.shift()
144
+ }
145
+ this._emitUpdate(pubkeyHex)
146
+ }
147
+
148
+ /**
149
+ * Set the stake bond age for a peer.
150
+ * @param {string} pubkeyHex
151
+ * @param {number} days — age of stake bond in days
152
+ */
153
+ setStakeAge (pubkeyHex, days) {
154
+ const m = this._getMetrics(pubkeyHex)
155
+ m.stakeAgeDays = days
156
+ this._emitUpdate(pubkeyHex)
157
+ }
158
+
159
+ /**
160
+ * Compute the uptime sub-score (0-1).
161
+ * @param {object} m — metrics
162
+ * @returns {number}
163
+ */
164
+ _computeUptime (m) {
165
+ if (m.pings === 0) return 0.5 // neutral when no data
166
+ return m.pongs / m.pings
167
+ }
168
+
169
+ /**
170
+ * Compute the response time sub-score (0-1).
171
+ * 1.0 = <= 100ms, 0.0 = >= 5000ms, linear between.
172
+ * @param {object} m — metrics
173
+ * @returns {number}
174
+ */
175
+ _computeResponseTime (m) {
176
+ if (m.latencies.length === 0) return 0.5 // neutral when no data
177
+ const avg = m.latencies.reduce((a, b) => a + b, 0) / m.latencies.length
178
+ if (avg <= RT_FLOOR) return 1.0
179
+ if (avg >= RT_CEIL) return 0.0
180
+ return 1.0 - (avg - RT_FLOOR) / (RT_CEIL - RT_FLOOR)
181
+ }
182
+
183
+ /**
184
+ * Compute the data accuracy sub-score (0-1).
185
+ * @param {object} m — metrics
186
+ * @returns {number}
187
+ */
188
+ _computeDataAccuracy (m) {
189
+ if (m.accuracyLog.length === 0) return 0.5 // neutral when no data
190
+ const good = m.accuracyLog.filter(Boolean).length
191
+ return good / m.accuracyLog.length
192
+ }
193
+
194
+ /**
195
+ * Compute the stake age sub-score (0-1).
196
+ * Formula: log2(days) / 10, capped at 1.0.
197
+ * @param {object} m — metrics
198
+ * @returns {number}
199
+ */
200
+ _computeStakeAge (m) {
201
+ if (m.stakeAgeDays <= 0) return 0
202
+ return Math.min(1.0, Math.log2(m.stakeAgeDays) / STAKE_AGE_DIVISOR)
203
+ }
204
+
205
+ /**
206
+ * Get the composite score for a peer.
207
+ * @param {string} pubkeyHex
208
+ * @returns {number} 0-1
209
+ */
210
+ getScore (pubkeyHex) {
211
+ if (!this._peers.has(pubkeyHex)) return 0.5 // unknown peer = neutral
212
+
213
+ const m = this._peers.get(pubkeyHex)
214
+ const uptime = this._computeUptime(m)
215
+ const responseTime = this._computeResponseTime(m)
216
+ const dataAccuracy = this._computeDataAccuracy(m)
217
+ const stakeAge = this._computeStakeAge(m)
218
+
219
+ return (
220
+ WEIGHTS.uptime * uptime +
221
+ WEIGHTS.responseTime * responseTime +
222
+ WEIGHTS.dataAccuracy * dataAccuracy +
223
+ WEIGHTS.stakeAge * stakeAge
224
+ )
225
+ }
226
+
227
+ /**
228
+ * Get all sub-scores and raw metrics for a peer.
229
+ * @param {string} pubkeyHex
230
+ * @returns {object|null}
231
+ */
232
+ getMetrics (pubkeyHex) {
233
+ if (!this._peers.has(pubkeyHex)) return null
234
+
235
+ const m = this._peers.get(pubkeyHex)
236
+ return {
237
+ uptime: this._computeUptime(m),
238
+ responseTime: this._computeResponseTime(m),
239
+ dataAccuracy: this._computeDataAccuracy(m),
240
+ stakeAge: this._computeStakeAge(m),
241
+ score: this.getScore(pubkeyHex),
242
+ raw: {
243
+ pings: m.pings,
244
+ pongs: m.pongs,
245
+ latencySamples: m.latencies.length,
246
+ avgLatencyMs: m.latencies.length > 0
247
+ ? Math.round(m.latencies.reduce((a, b) => a + b, 0) / m.latencies.length)
248
+ : null,
249
+ accuracySamples: m.accuracyLog.length,
250
+ stakeAgeDays: m.stakeAgeDays
251
+ }
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Get scores for all tracked peers.
257
+ * @returns {Map<string, number>}
258
+ */
259
+ getAllScores () {
260
+ const scores = new Map()
261
+ for (const pubkeyHex of this._peers.keys()) {
262
+ scores.set(pubkeyHex, this.getScore(pubkeyHex))
263
+ }
264
+ return scores
265
+ }
266
+
267
+ /**
268
+ * Remove a peer from tracking.
269
+ * @param {string} pubkeyHex
270
+ */
271
+ removePeer (pubkeyHex) {
272
+ this._peers.delete(pubkeyHex)
273
+ }
274
+
275
+ /**
276
+ * Emit a score update event.
277
+ * @param {string} pubkeyHex
278
+ */
279
+ _emitUpdate (pubkeyHex) {
280
+ this.emit('score:update', {
281
+ pubkeyHex,
282
+ score: this.getScore(pubkeyHex)
283
+ })
284
+ }
285
+ }