This commit is contained in:
Raven Scott
2026-05-20 18:23:22 -04:00
parent 46544f0a93
commit e1fb64e484
28 changed files with 553 additions and 98 deletions
+8 -5
View File
@@ -50,6 +50,7 @@ class HyperP2PDistributedEventBus extends EventEmitter {
this.expiryMs = opts.expiry || DEFAULT_EXPIRY this.expiryMs = opts.expiry || DEFAULT_EXPIRY
this.metadata = opts.metadata || { agent: 'hyper-p2p-distributed-event-bus' } this.metadata = opts.metadata || { agent: 'hyper-p2p-distributed-event-bus' }
this.enableSigning = opts.enableSigning !== false this.enableSigning = opts.enableSigning !== false
this._enableBackgroundTimers = opts.enableBackgroundTimers === true
this._metrics = { published: 0, received: 0, signed: 0, verified: 0, pruned: 0, errors: 0 } this._metrics = { published: 0, received: 0, signed: 0, verified: 0, pruned: 0, errors: 0 }
// Internal structures // Internal structures
@@ -81,8 +82,10 @@ class HyperP2PDistributedEventBus extends EventEmitter {
if (this._joined) return this if (this._joined) return this
await this._initStorage() await this._initStorage()
await this._initSwarm() await this._initSwarm()
this._startAnnounceTimer() if (this._enableBackgroundTimers) {
this._startCleanupTimer() this._startAnnounceTimer()
this._startCleanupTimer()
}
this._joined = true this._joined = true
this.emit('ready') this.emit('ready')
return this return this
@@ -144,12 +147,12 @@ class HyperP2PDistributedEventBus extends EventEmitter {
const peerPub = peerInfo.publicKey || connection.remotePublicKey const peerPub = peerInfo.publicKey || connection.remotePublicKey
const peerHex = peerPub ? b4a.toString(peerPub, 'hex') : b4a.toString(crypto.randomBytes(32), 'hex') const peerHex = peerPub ? b4a.toString(peerPub, 'hex') : b4a.toString(crypto.randomBytes(32), 'hex')
const self = this const self = this
this.emit('peer-connected', { peer: peerHex })
const { channel, msg } = protocolChannel(mux, { protocolChannel(mux, {
protocol: EVENT_BUS_PROTOCOL, protocol: EVENT_BUS_PROTOCOL,
onopen () { onopen (channel, msg) {
self.peers.set(peerHex, { lastSeen: Date.now(), metadata: {}, channel, msg }) self.peers.set(peerHex, { lastSeen: Date.now(), metadata: {}, channel, msg })
self.emit('peer-connected', { peer: peerHex })
}, },
onclose () { onclose () {
self.peers.delete(peerHex) self.peers.delete(peerHex)
+20 -6
View File
@@ -46,6 +46,8 @@ class HyperP2PDistributedLock extends EventEmitter {
this._protocol = null this._protocol = null
this._peerMsgs = new Map() this._peerMsgs = new Map()
this._cleanupTimer = null this._cleanupTimer = null
this._leaseTimers = new Set()
this._enableBackgroundTimers = opts.enableBackgroundTimers === true
this._joined = false this._joined = false
this.metrics = { this.metrics = {
acquiresAttempted: 0, acquiresAttempted: 0,
@@ -93,7 +95,7 @@ class HyperP2PDistributedLock extends EventEmitter {
if (this._joined) return if (this._joined) return
await this._initStorage() await this._initStorage()
await this._initP2P() await this._initP2P()
this._startCleanup() if (this._enableBackgroundTimers) this._startCleanup()
this._joined = true this._joined = true
this.emit('ready') this.emit('ready')
} }
@@ -108,6 +110,7 @@ class HyperP2PDistributedLock extends EventEmitter {
async _initP2P () { async _initP2P () {
this._peerMsgs = new Map() this._peerMsgs = new Map()
this.peers = this._peerMsgs
if (!this.topic) { if (!this.topic) {
this._protocol = null this._protocol = null
return return
@@ -116,8 +119,15 @@ class HyperP2PDistributedLock extends EventEmitter {
this.swarm = swarm this.swarm = swarm
const self = this const self = this
wireConnection(swarm, (socket, peerInfo, mux) => { wireConnection(swarm, (socket, peerInfo, mux) => {
const { msg } = protocolChannel(mux, { const peerHex = peerInfo.publicKey ? b4a.toString(peerInfo.publicKey, 'hex') : null
protocolChannel(mux, {
protocol: LOCK_PROTOCOL, protocol: LOCK_PROTOCOL,
onopen (channel, msg) {
if (peerHex) {
self._peerMsgs.set(peerHex, { msg })
self.emit('peer-connected', { peer: peerHex })
}
},
onmessage (data) { onmessage (data) {
if (!data || !data.type) return if (!data || !data.type) return
if (data.type === 'claim' && data.resourceId && data.claim) { if (data.type === 'claim' && data.resourceId && data.claim) {
@@ -128,10 +138,11 @@ class HyperP2PDistributedLock extends EventEmitter {
self._forceRelease(data.resourceId, lock.lockId, 'remote-release') self._forceRelease(data.resourceId, lock.lockId, 'remote-release')
} }
} }
},
onclose () {
if (peerHex) self._peerMsgs.delete(peerHex)
} }
}) })
const peerHex = peerInfo.publicKey ? b4a.toString(peerInfo.publicKey, 'hex') : null
if (peerHex) self._peerMsgs.set(peerHex, msg)
socket.on('close', () => { socket.on('close', () => {
if (peerHex) self._peerMsgs.delete(peerHex) if (peerHex) self._peerMsgs.delete(peerHex)
}) })
@@ -208,12 +219,13 @@ class HyperP2PDistributedLock extends EventEmitter {
this.metrics.acquiresSucceeded++ this.metrics.acquiresSucceeded++
this.emit('acquired', { resourceId, lockId, fencingToken, expiresAt, owner: this.publicKeyHex }) this.emit('acquired', { resourceId, lockId, fencingToken, expiresAt, owner: this.publicKeyHex })
// Auto-release on timeout for owner (safety) const leaseTimer = setTimeout(() => {
setTimeout(() => { this._leaseTimers.delete(leaseTimer)
if (this.locks.get(resourceId)?.lockId === lockId && Date.now() >= expiresAt) { if (this.locks.get(resourceId)?.lockId === lockId && Date.now() >= expiresAt) {
this._forceRelease(resourceId, lockId, 'lease-expired') this._forceRelease(resourceId, lockId, 'lease-expired')
} }
}, leaseMs + 1000) }, leaseMs + 1000)
this._leaseTimers.add(leaseTimer)
return { lockId, fencingToken, acquiredAt: lockInfo.acquiredAt, expiresAt, owner: this.publicKeyHex, resourceId } return { lockId, fencingToken, acquiredAt: lockInfo.acquiredAt, expiresAt, owner: this.publicKeyHex, resourceId }
} }
@@ -379,6 +391,8 @@ class HyperP2PDistributedLock extends EventEmitter {
} }
async close () { async close () {
for (const t of this._leaseTimers) clearTimeout(t)
this._leaseTimers.clear()
if (this._cleanupTimer) { if (this._cleanupTimer) {
clearInterval(this._cleanupTimer) clearInterval(this._cleanupTimer)
this._cleanupTimer = null this._cleanupTimer = null
+8 -5
View File
@@ -35,6 +35,7 @@ class HyperP2PIntentRouter extends EventEmitter {
this.maxIntentsPerPeer = opts.maxIntentsPerPeer || 64 this.maxIntentsPerPeer = opts.maxIntentsPerPeer || 64
this.matchThreshold = opts.matchThreshold || 0.3 // Jaccard-like similarity this.matchThreshold = opts.matchThreshold || 0.3 // Jaccard-like similarity
this.options = opts this.options = opts
this._enableBackgroundTimers = opts.enableBackgroundTimers === true
this.localIntents = new Map() // intentId -> intent this.localIntents = new Map() // intentId -> intent
this.peerIntents = new Map() // peerPubHex -> { intents: Map, lastSeen: ts, connections: Set } this.peerIntents = new Map() // peerPubHex -> { intents: Map, lastSeen: ts, connections: Set }
@@ -46,6 +47,7 @@ class HyperP2PIntentRouter extends EventEmitter {
this._joined = false this._joined = false
this._protocol = null this._protocol = null
this._connections = new Map() // peerPubHex -> { msg, channel } this._connections = new Map() // peerPubHex -> { msg, channel }
this.peers = this._connections
this._topics = new Map() // topicHex -> swarm topic handle this._topics = new Map() // topicHex -> swarm topic handle
} }
@@ -54,8 +56,10 @@ class HyperP2PIntentRouter extends EventEmitter {
await this._initStorage() await this._initStorage()
await this._initSwarm() await this._initSwarm()
await this._loadPersistedIntents() await this._loadPersistedIntents()
this._startAnnounceTimer() if (this._enableBackgroundTimers) {
this._startCleanupTimer() this._startAnnounceTimer()
this._startCleanupTimer()
}
this._joined = true this._joined = true
this.emit('ready') this.emit('ready')
} }
@@ -302,7 +306,8 @@ class HyperP2PIntentRouter extends EventEmitter {
const { channel, msg } = protocolChannel(mux, { const { channel, msg } = protocolChannel(mux, {
protocol: INTENT_PROTOCOL, protocol: INTENT_PROTOCOL,
onopen () { onopen (channel, msg) {
self._connections.set(peerHex, { msg, channel })
self.emit('peer:connected', { peerPublicKey: peerHex }) self.emit('peer:connected', { peerPublicKey: peerHex })
self._exchangeIntents(peerHex, msg) self._exchangeIntents(peerHex, msg)
}, },
@@ -319,8 +324,6 @@ class HyperP2PIntentRouter extends EventEmitter {
} }
}) })
this._connections.set(peerHex, { msg, channel })
if (!this.peerIntents.has(peerHex)) { if (!this.peerIntents.has(peerHex)) {
this.peerIntents.set(peerHex, { intents: new Map(), lastSeen: Date.now(), connections: new Set() }) this.peerIntents.set(peerHex, { intents: new Map(), lastSeen: Date.now(), connections: new Set() })
} }
+5 -3
View File
@@ -27,6 +27,7 @@ class HyperP2PPresence extends EventEmitter {
this.announceIntervalMs = opts.announceInterval || DEFAULT_ANNOUNCE_INTERVAL this.announceIntervalMs = opts.announceInterval || DEFAULT_ANNOUNCE_INTERVAL
this.expiryMs = opts.expiry || DEFAULT_EXPIRY this.expiryMs = opts.expiry || DEFAULT_EXPIRY
this.metadata = opts.metadata || {} this.metadata = opts.metadata || {}
this._enableBackgroundTimers = opts.enableBackgroundTimers === true
this.peers = new Map() // publicKeyHex -> presence info this.peers = new Map() // publicKeyHex -> presence info
this.swarm = null this.swarm = null
this.corestore = null this.corestore = null
@@ -56,7 +57,10 @@ class HyperP2PPresence extends EventEmitter {
await this._initStorage() await this._initStorage()
await this._initSwarm() await this._initSwarm()
this._ensureSelfRegistered() this._ensureSelfRegistered()
this._startCleanupTimer() if (this._enableBackgroundTimers) {
this._startAnnounceTimer()
this._startCleanupTimer()
}
this._joined = true this._joined = true
this.emit('ready') this.emit('ready')
} }
@@ -109,8 +113,6 @@ class HyperP2PPresence extends EventEmitter {
wireConnection(this.swarm, (socket, info, mux) => { wireConnection(this.swarm, (socket, info, mux) => {
this._handleConnection(socket, info, mux) this._handleConnection(socket, info, mux)
}) })
this._startAnnounceTimer()
} }
_handleConnection (socket, info, mux) { _handleConnection (socket, info, mux) {
+13 -8
View File
@@ -45,6 +45,8 @@ class HyperP2PReactiveState extends EventEmitter {
this.syncIntervalMs = opts.syncInterval || DEFAULT_SYNC_INTERVAL this.syncIntervalMs = opts.syncInterval || DEFAULT_SYNC_INTERVAL
this.expiryMs = opts.expiry || DEFAULT_EXPIRY this.expiryMs = opts.expiry || DEFAULT_EXPIRY
this.metadata = opts.metadata || { agent: 'hyper-p2p-reactive-state' } this.metadata = opts.metadata || { agent: 'hyper-p2p-reactive-state' }
this._enableBackgroundTimers = opts.enableBackgroundTimers === true
this._memoryOnly = opts.memoryOnly === true
// Internal state: key -> { value, timestamp, peerId, signature? } // Internal state: key -> { value, timestamp, peerId, signature? }
this.state = new Map() this.state = new Map()
@@ -73,14 +75,17 @@ class HyperP2PReactiveState extends EventEmitter {
if (this._joined) return this if (this._joined) return this
await this._initStorage() await this._initStorage()
await this._initSwarm() await this._initSwarm()
this._startSyncTimer() if (this._enableBackgroundTimers) {
this._startCleanupTimer() this._startSyncTimer()
this._startCleanupTimer()
}
this._joined = true this._joined = true
this.emit('ready') this.emit('ready')
return this return this
} }
async _initStorage () { async _initStorage () {
if (this._memoryOnly) return
try { try {
await fs.mkdir(this.storageDir, { recursive: true }) await fs.mkdir(this.storageDir, { recursive: true })
} catch (err) { } catch (err) {
@@ -112,9 +117,7 @@ class HyperP2PReactiveState extends EventEmitter {
} }
async _initSwarm () { async _initSwarm () {
if (!this.topic) { if (!this.topic) return
throw new Error('topic is required for P2P discovery')
}
const { swarm } = await createSwarm({ keyPair: this.keyPair, topic: this.topic }) const { swarm } = await createSwarm({ keyPair: this.keyPair, topic: this.topic })
this.swarm = swarm this.swarm = swarm
@@ -215,6 +218,7 @@ class HyperP2PReactiveState extends EventEmitter {
} }
async _persistState () { async _persistState () {
if (!this.bee) return
for (const [key, data] of this.state) { for (const [key, data] of this.state) {
try { try {
await this.bee.put(LWW_PREFIX + key, data) await this.bee.put(LWW_PREFIX + key, data)
@@ -278,8 +282,7 @@ class HyperP2PReactiveState extends EventEmitter {
this.emit('change', { key, value, peer: this.publicKeyHex, type: 'set' }) this.emit('change', { key, value, peer: this.publicKeyHex, type: 'set' })
this.emit('set', { key, value, peer: this.publicKeyHex }) this.emit('set', { key, value, peer: this.publicKeyHex })
// Persist if (this.bee) await this.bee.put(LWW_PREFIX + key, data)
await this.bee.put(LWW_PREFIX + key, data)
// Broadcast update to peers (simplified - in prod would use open channels) // Broadcast update to peers (simplified - in prod would use open channels)
this._broadcastUpdate(key, data) this._broadcastUpdate(key, data)
@@ -321,7 +324,7 @@ class HyperP2PReactiveState extends EventEmitter {
this.emit('change', { key, value: null, peer: this.publicKeyHex, type: 'delete' }) this.emit('change', { key, value: null, peer: this.publicKeyHex, type: 'delete' })
this.emit('delete', { key, peer: this.publicKeyHex }) this.emit('delete', { key, peer: this.publicKeyHex })
await this.bee.put(LWW_PREFIX + key, data) if (this.bee) await this.bee.put(LWW_PREFIX + key, data)
this._broadcastUpdate(key, data) this._broadcastUpdate(key, data)
return true return true
} }
@@ -388,7 +391,9 @@ class HyperP2PReactiveState extends EventEmitter {
await this.swarm.destroy().catch(() => {}) await this.swarm.destroy().catch(() => {})
} }
if (this.bee) { if (this.bee) {
const core = this.bee.core
await this.bee.close().catch(() => {}) await this.bee.close().catch(() => {})
if (core && !core.closed) await core.close().catch(() => {})
} }
this._joined = false this._joined = false
this.emit('closed') this.emit('closed')
@@ -0,0 +1,23 @@
{
"scores": {
"9a7447a62939fed8972a21584358ae841176d9bea5a778ee595eb0dcc974faef": {
"score": 75,
"lastUpdated": 1779313150686,
"attestationsCount": 1
}
},
"history": {
"9a7447a62939fed8972a21584358ae841176d9bea5a778ee595eb0dcc974faef": [
{
"ts": 1779313150686,
"delta": 75,
"attester": "b553633830fad5f0ae72dfb4777ad1c30a1f631be1b7f9c8b69a57d82d99f1d2",
"signature": "/S3yN5f7BiMDQ4Ia/3tyS84NisrHn1YQqBkQ6bz0wqqselX6szZ4u+xWr1Snwp4yZ3NHKhIU4/CNLARSUk9eBw==",
"metadata": {
"context": "test"
}
}
]
},
"lastPersist": 1779313150746
}
@@ -0,0 +1,23 @@
{
"scores": {
"7aad1180ce02770a762e8ade23298a72b7fb69a410aaf96967cf4cc8bbe263f7": {
"score": 75,
"lastUpdated": 1779314988625,
"attestationsCount": 1
}
},
"history": {
"7aad1180ce02770a762e8ade23298a72b7fb69a410aaf96967cf4cc8bbe263f7": [
{
"ts": 1779314988625,
"delta": 75,
"attester": "54c78e088fa0763e649487d92cc567e4438dea71b62e4a798899ea4ee3581f08",
"signature": "fKEF7p7sK8biri3SKW2HLLWHKbmtPbFTmfj+3kol9mPwYcs1Nw9KS9gx+HJCuU93rYLmvVqtsZAoR154py0SCg==",
"metadata": {
"context": "test"
}
}
]
},
"lastPersist": 1779314988683
}
@@ -0,0 +1,21 @@
{
"scores": {
"decay-peer": {
"score": 90,
"lastUpdated": 1779313150749,
"attestationsCount": 1
}
},
"history": {
"decay-peer": [
{
"ts": 1779313150749,
"delta": 100,
"attester": "7acd28b48a42bdd04a0f3998666195a59f6985159678fea2664a991325924629",
"signature": "NwMPLq/7XFlrlTWvvm7WgKHTGIR0e2qwfbDGCgE+wLSYzTiul4KFZrh0G9H8rNzW93Qofq91qtZLSlkRi66MDQ==",
"metadata": {}
}
]
},
"lastPersist": 1779313150750
}
@@ -0,0 +1,21 @@
{
"scores": {
"decay-peer": {
"score": 90,
"lastUpdated": 1779314988686,
"attestationsCount": 1
}
},
"history": {
"decay-peer": [
{
"ts": 1779314988686,
"delta": 100,
"attester": "a25f07e97664f33d97a123e00bba1b6f445b581dc762f10c799dfd9037505a2e",
"signature": "JY0WvtXqMvEugNw3Ju5t4VoNMDMNTe/wcwNd/SHXSWZOs09wkrw91uGQlN7M15+PLk2fQfMve+r3XfOC8ijAAg==",
"metadata": {}
}
]
},
"lastPersist": 1779314988687
}
@@ -0,0 +1,5 @@
{
"scores": {},
"history": {},
"lastPersist": 1779313150684
}
@@ -0,0 +1,5 @@
{
"scores": {},
"history": {},
"lastPersist": 1779314988623
}
@@ -0,0 +1,21 @@
{
"scores": {
"persist-peer": {
"score": 55,
"lastUpdated": 1779313150750,
"attestationsCount": 1
}
},
"history": {
"persist-peer": [
{
"ts": 1779313150750,
"delta": 55,
"attester": "0fdb685f0288995edeab58e47aa89293b53ed542256729cc28f8a4d36a0c09f6",
"signature": "jN/+Z+WZu8D/HNCUfap6i1kXmunAx/ja9y5ZwNKY3Tt30+p8Fu2dD1Cw3ypLPwMqtsTSB0TrZkJCjYxoQiH2Cg==",
"metadata": {}
}
]
},
"lastPersist": 1779313150751
}
@@ -0,0 +1,21 @@
{
"scores": {
"persist-peer": {
"score": 55,
"lastUpdated": 1779314988687,
"attestationsCount": 1
}
},
"history": {
"persist-peer": [
{
"ts": 1779314988687,
"delta": 55,
"attester": "dbd358d15af6021800b1e2d0be35d467946677f1191035775688a47c451232af",
"signature": "ZT+iAPrcUpo5vKLVJavv8QhZevzw1oWPpue5Q0dA2TejxqZmzO3ZDYr349gEskosg9nx9QphR3xQCzGmEeGFDQ==",
"metadata": {}
}
]
},
"lastPersist": 1779314988688
}
@@ -0,0 +1,35 @@
{
"scores": {
"peer1": {
"score": 100,
"lastUpdated": 1779313150747,
"attestationsCount": 1
},
"peer2": {
"score": 60,
"lastUpdated": 1779313150748,
"attestationsCount": 1
}
},
"history": {
"peer1": [
{
"ts": 1779313150747,
"delta": 100,
"attester": "89e865aac9e474c3ddeed73a45c1e4ef73f5d8d5c3e0194056b0ad86893b78d8",
"signature": "btVkpLdN5N44isEU2u5i8aF9Ge8YZ42GHqofOwZxaj+YePsg4C0BHP2ojg/NmoddjKWZuaPq5ifby5zNi8vVCQ==",
"metadata": {}
}
],
"peer2": [
{
"ts": 1779313150748,
"delta": 60,
"attester": "89e865aac9e474c3ddeed73a45c1e4ef73f5d8d5c3e0194056b0ad86893b78d8",
"signature": "WkfnZOAW7j3LbxA6voXlGFednXObrfgBjHEiS78+iLlrdvPFJE9qI/uA5MM82BsIi4/wil0fkiG1vsSAYRgdCg==",
"metadata": {}
}
]
},
"lastPersist": 1779313150748
}
@@ -0,0 +1,35 @@
{
"scores": {
"peer1": {
"score": 100,
"lastUpdated": 1779314988685,
"attestationsCount": 1
},
"peer2": {
"score": 60,
"lastUpdated": 1779314988685,
"attestationsCount": 1
}
},
"history": {
"peer1": [
{
"ts": 1779314988685,
"delta": 100,
"attester": "ccc9bea7120a8d19a79a68dfe0db251882f52eae3b83dbfd5cfbd5a7439da21a",
"signature": "K5E220bP18H4c7oQem+h2voSl3nEOvI0sV2dHSwlgtnuig0UOqh9joxbGUaof+m/Mwulf8GrWwZ46BUjS10JAw==",
"metadata": {}
}
],
"peer2": [
{
"ts": 1779314988685,
"delta": 60,
"attester": "ccc9bea7120a8d19a79a68dfe0db251882f52eae3b83dbfd5cfbd5a7439da21a",
"signature": "RE+TyQw1wiqS93togQpXRuXrpAXV4kiCuErXjfTJEjNFo3qeEE6s5HsX0Mb5yBLca8rL5SWifwh1a3PHzB4bBg==",
"metadata": {}
}
]
},
"lastPersist": 1779314988686
}
@@ -0,0 +1,5 @@
{
"scores": {},
"history": {},
"lastPersist": 1779313150747
}
@@ -0,0 +1,5 @@
{
"scores": {},
"history": {},
"lastPersist": 1779314988684
}
@@ -0,0 +1,37 @@
{
"scores": {
"snapshot-peer-1": {
"score": 120,
"lastUpdated": 1779313150752,
"attestationsCount": 1
},
"snapshot-peer-2": {
"score": 80,
"lastUpdated": 1779313150752,
"attestationsCount": 1
}
},
"history": {
"snapshot-peer-1": [
{
"ts": 1779313150752,
"delta": 120,
"attester": "8550888040dc7099af6bd90144a43c6e0ba78c95551696152b18f34c46d53904",
"signature": "LK7bflK96+Jh+M/ZzPMLjTJO5J39kVtsaD4WplQOuXiBbcIz+RdBmDAZ1eez9XFw4Vt5RBtbmiEe1I72c9XfDw==",
"metadata": {
"category": "reliability"
}
}
],
"snapshot-peer-2": [
{
"ts": 1779313150752,
"delta": 80,
"attester": "8550888040dc7099af6bd90144a43c6e0ba78c95551696152b18f34c46d53904",
"signature": "5wYmYD8JQ52mgH+zJTECClxS6qKKqiPQ2VrPQasw9wYjVgHnyjUIouANAP067ZQsW0dbXRSXnCiLd7GTUF2IAw==",
"metadata": {}
}
]
},
"lastPersist": 1779313150753
}
@@ -0,0 +1,37 @@
{
"scores": {
"snapshot-peer-1": {
"score": 120,
"lastUpdated": 1779314988689,
"attestationsCount": 1
},
"snapshot-peer-2": {
"score": 80,
"lastUpdated": 1779314988689,
"attestationsCount": 1
}
},
"history": {
"snapshot-peer-1": [
{
"ts": 1779314988689,
"delta": 120,
"attester": "0294f3f95780a29da1bba2808769869655894c041f1c8f206f5816cfce561354",
"signature": "AYp8BMiEZwGuM9xLFW8aFtwaLukXEgYTAV+uG8VEi/ZX2QsYnqY4u4NcV4zGm5Bbdt9AGg8MBcbC0gWiAPcYAQ==",
"metadata": {
"category": "reliability"
}
}
],
"snapshot-peer-2": [
{
"ts": 1779314988689,
"delta": 80,
"attester": "0294f3f95780a29da1bba2808769869655894c041f1c8f206f5816cfce561354",
"signature": "pufS+vOSUwtby+rH2R1tdvs/+SWO8xa0Jx/+1DzS9HNkIUkcK6kZOeU1CUOT4ndvSkrVI53bGEaMWtxJ+yNqDA==",
"metadata": {}
}
]
},
"lastPersist": 1779314988690
}
@@ -0,0 +1,37 @@
{
"scores": {
"snapshot-peer-1": {
"score": 120,
"lastUpdated": 1779313150752,
"attestationsCount": 1
},
"snapshot-peer-2": {
"score": 80,
"lastUpdated": 1779313150752,
"attestationsCount": 1
}
},
"history": {
"snapshot-peer-1": [
{
"ts": 1779313150752,
"delta": 120,
"attester": "8550888040dc7099af6bd90144a43c6e0ba78c95551696152b18f34c46d53904",
"signature": "LK7bflK96+Jh+M/ZzPMLjTJO5J39kVtsaD4WplQOuXiBbcIz+RdBmDAZ1eez9XFw4Vt5RBtbmiEe1I72c9XfDw==",
"metadata": {
"category": "reliability"
}
}
],
"snapshot-peer-2": [
{
"ts": 1779313150752,
"delta": 80,
"attester": "8550888040dc7099af6bd90144a43c6e0ba78c95551696152b18f34c46d53904",
"signature": "5wYmYD8JQ52mgH+zJTECClxS6qKKqiPQ2VrPQasw9wYjVgHnyjUIouANAP067ZQsW0dbXRSXnCiLd7GTUF2IAw==",
"metadata": {}
}
]
},
"lastPersist": 1779313150753
}
@@ -0,0 +1,37 @@
{
"scores": {
"snapshot-peer-1": {
"score": 120,
"lastUpdated": 1779314988689,
"attestationsCount": 1
},
"snapshot-peer-2": {
"score": 80,
"lastUpdated": 1779314988689,
"attestationsCount": 1
}
},
"history": {
"snapshot-peer-1": [
{
"ts": 1779314988689,
"delta": 120,
"attester": "0294f3f95780a29da1bba2808769869655894c041f1c8f206f5816cfce561354",
"signature": "AYp8BMiEZwGuM9xLFW8aFtwaLukXEgYTAV+uG8VEi/ZX2QsYnqY4u4NcV4zGm5Bbdt9AGg8MBcbC0gWiAPcYAQ==",
"metadata": {
"category": "reliability"
}
}
],
"snapshot-peer-2": [
{
"ts": 1779314988689,
"delta": 80,
"attester": "0294f3f95780a29da1bba2808769869655894c041f1c8f206f5816cfce561354",
"signature": "pufS+vOSUwtby+rH2R1tdvs/+SWO8xa0Jx/+1DzS9HNkIUkcK6kZOeU1CUOT4ndvSkrVI53bGEaMWtxJ+yNqDA==",
"metadata": {}
}
]
},
"lastPersist": 1779314988690
}
+9 -1
View File
@@ -108,4 +108,12 @@ npm install
npx brittle-bare test/test.js npx brittle-bare test/test.js
``` ```
Integration (2-node): [`../../real_tests/integration/`](../../real_tests/integration/) — see [DEVELOPMENT.md](../../DEVELOPMENT.md). ### SecretStream pairing (in-process)
```js
const { pairSecretStreams, waitSecretStreamsConnected } = require('../lib/pair.js')
pairSecretStreams(initiatorStream, responderStream)
await waitSecretStreamsConnected(initiatorStream, responderStream)
```
Integration: `npx brittle-bare test/integration-two-node.js` (from this package). See [DEVELOPMENT.md](../../../DEVELOPMENT.md).
+5 -13
View File
@@ -1,17 +1,13 @@
require('bare-process/global') require('bare-process/global')
const { RPCServer, RPCClient } = require('../index.js') const { RPCServer, RPCClient } = require('../index.js')
const SecretStream = require('@hyperswarm/secret-stream') const SecretStream = require('@hyperswarm/secret-stream')
const hypercoreCrypto = require('hypercore-crypto') const { pairSecretStreams, waitSecretStreamsConnected } = require('../lib/pair.js')
async function demo () { async function demo () {
console.log('=== hyper-p2p-rpc Basic Demo (SecretStream) ===') console.log('=== hyper-p2p-rpc Basic Demo (SecretStream) ===')
const kp = hypercoreCrypto.keyPair() const a = new SecretStream(true)
const a = new SecretStream(true, null, { keyPair: kp }) const b = new SecretStream(false)
const b = new SecretStream(false, null, { keyPair: hypercoreCrypto.keyPair() })
a.isInitiator = true
b.isInitiator = false
const server = new RPCServer() const server = new RPCServer()
server.register('add', async (params) => ({ result: (params.a || 0) + (params.b || 0) })) server.register('add', async (params) => ({ result: (params.a || 0) + (params.b || 0) }))
@@ -20,12 +16,8 @@ async function demo () {
server.handleConnection(a) server.handleConnection(a)
const client = new RPCClient(b) const client = new RPCClient(b)
await new Promise((resolve, reject) => { pairSecretStreams(a, b)
a.on('connect', resolve) await waitSecretStreamsConnected(a, b)
b.on('connect', resolve)
a.connect(b)
setTimeout(() => reject(new Error('connect timeout')), 5000)
})
const sum = await client.call('add', { a: 42, b: 58 }) const sum = await client.call('add', { a: 42, b: 58 })
console.log('42 + 58 =', sum.result) console.log('42 + 58 =', sum.result)
+5 -12
View File
@@ -1,15 +1,13 @@
require('bare-process/global') require('bare-process/global')
const { RPCServer, RPCClient } = require('../index.js') const { RPCServer, RPCClient } = require('../index.js')
const SecretStream = require('@hyperswarm/secret-stream') const SecretStream = require('@hyperswarm/secret-stream')
const hypercoreCrypto = require('hypercore-crypto') const { pairSecretStreams, waitSecretStreamsConnected } = require('../lib/pair.js')
const { setInterval, clearInterval } = require('bare-timers')
async function streamingDemo () { async function streamingDemo () {
console.log('=== hyper-p2p-rpc Streaming Demo (SecretStream) ===') console.log('=== hyper-p2p-rpc Streaming Demo (SecretStream) ===')
const kp = hypercoreCrypto.keyPair() const a = new SecretStream(true)
const a = new SecretStream(true, null, { keyPair: kp }) const b = new SecretStream(false)
const b = new SecretStream(false, null, { keyPair: hypercoreCrypto.keyPair() })
const server = new RPCServer() const server = new RPCServer()
server.register('count-stream', async function * (params) { server.register('count-stream', async function * (params) {
@@ -23,13 +21,8 @@ async function streamingDemo () {
server.handleConnection(a) server.handleConnection(a)
const client = new RPCClient(b) const client = new RPCClient(b)
await new Promise((resolve, reject) => { pairSecretStreams(a, b)
const timer = setTimeout(() => reject(new Error('connect timeout')), 5000) await waitSecretStreamsConnected(a, b)
const done = () => { clearTimeout(timer); resolve() }
a.on('connect', done)
b.on('connect', done)
a.connect(b)
})
const stream = await client.callStream('count-stream', { max: 4 }) const stream = await client.callStream('count-stream', { max: 4 })
for await (const chunk of stream) { for await (const chunk of stream) {
+48 -45
View File
@@ -72,7 +72,7 @@ class RPCServer extends EventEmitter {
if (result && typeof result[Symbol.asyncIterator] === 'function') { if (result && typeof result[Symbol.asyncIterator] === 'function') {
rpcMsg.send({ id: msg.id, stream: true }) rpcMsg.send({ id: msg.id, stream: true })
await this._handleStreamingResponse(socket, msg.id, result) await this._streamOnRpcChannel(rpcMsg, msg.id, result)
} else { } else {
rpcMsg.send({ id: msg.id, result }) rpcMsg.send({ id: msg.id, result })
} }
@@ -81,22 +81,14 @@ class RPCServer extends EventEmitter {
} }
} }
async _handleStreamingResponse (socket, callId, sourceStream) { async _streamOnRpcChannel (rpcMsg, callId, sourceStream) {
const mux = Protomux.from(socket)
const streamChannel = mux.createChannel({
protocol: STREAM_PROTOCOL + '/' + callId
})
const dataMsg = streamChannel.addMessage({ encoding: c.json })
streamChannel.open()
try { try {
for await (const chunk of sourceStream) { for await (const chunk of sourceStream) {
dataMsg.send({ id: callId, chunk }) rpcMsg.send({ id: callId, chunk })
} }
dataMsg.send({ id: callId, done: true }) rpcMsg.send({ id: callId, done: true })
} catch (err) { } catch (err) {
dataMsg.send({ id: callId, error: err.message }) rpcMsg.send({ id: callId, error: err.message })
} }
} }
@@ -153,7 +145,18 @@ class RPCClient extends EventEmitter {
onmessage (reply) { onmessage (reply) {
const pending = self.pending.get(reply.id) const pending = self.pending.get(reply.id)
if (!pending) return if (!pending) return
if (reply.stream) return if (reply.stream) {
if (pending.streamReady) {
clearTimeout(pending.timer)
pending.streamReady()
}
return
}
if (reply.chunk !== undefined || reply.done || reply.error) {
if (pending.onStreamMsg) pending.onStreamMsg(reply)
if (reply.done || reply.error) self.pending.delete(reply.id)
return
}
clearTimeout(pending.timer) clearTimeout(pending.timer)
self.pending.delete(reply.id) self.pending.delete(reply.id)
@@ -188,48 +191,48 @@ class RPCClient extends EventEmitter {
async callStream (method, params = {}, timeoutMs = this.defaultTimeout) { async callStream (method, params = {}, timeoutMs = this.defaultTimeout) {
const id = generateId() const id = generateId()
const queue = []
const waiters = []
return new Promise((resolve, reject) => { const push = (msg) => {
if (waiters.length) waiters.shift()(msg)
else queue.push(msg)
}
await new Promise((resolve, reject) => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
this.pending.delete(id) this.pending.delete(id)
reject(new Error('STREAM_TIMEOUT')) reject(new Error('STREAM_TIMEOUT'))
}, timeoutMs) }, timeoutMs)
const mux = this.mux this.pending.set(id, {
const streamChannel = mux.createChannel({ reject,
protocol: STREAM_PROTOCOL + '/' + id timer,
streamReady: resolve,
onStreamMsg: push
}) })
this.rpcMsg.send({ id, method, params })
})
const dataMsg = streamChannel.addMessage({ encoding: c.json }) const next = () => new Promise((resolve, reject) => {
streamChannel.open() if (queue.length) return resolve(queue.shift())
const t = setTimeout(() => reject(new Error('stream read timeout')), timeoutMs)
waiters.push((msg) => {
clearTimeout(t)
resolve(msg)
})
})
let ended = false return {
[Symbol.asyncIterator]: async function * () {
const readable = { while (true) {
[Symbol.asyncIterator]: async function * () { const data = await next()
while (!ended) { if (data.error) throw new Error(data.error)
const data = await new Promise((res, rej) => { if (data.done) return
const t = setTimeout(() => rej(new Error('stream read timeout')), 5000) if (data.chunk !== undefined) yield data.chunk
const handler = (msg) => {
clearTimeout(t)
res(msg)
}
dataMsg.onmessage = handler
})
if (data.done) {
ended = true
return
}
if (data.error) throw new Error(data.error)
if (data.chunk !== undefined) yield data.chunk
}
} }
} }
}
this.rpcMsg.send({ id, method, params, stream: true })
resolve(readable)
})
} }
close () { close () {
+10
View File
@@ -0,0 +1,10 @@
/** Pair two SecretStream instances in-process (Protomux-style loopback). */
function pairSecretStreams (initiator, responder) {
initiator.rawStream.pipe(responder.rawStream).pipe(initiator.rawStream)
}
async function waitSecretStreamsConnected (...streams) {
await Promise.all(streams.map((s) => s.opened))
}
module.exports = { pairSecretStreams, waitSecretStreamsConnected }
@@ -0,0 +1,25 @@
require('bare-process/global')
const test = require('brittle')
const SecretStream = require('@hyperswarm/secret-stream')
const { RPCServer, RPCClient } = require('../index.js')
const { pairSecretStreams, waitSecretStreamsConnected } = require('../lib/pair.js')
test('rpc: call over SecretStream pair', async function (t) {
const a = new SecretStream(true)
const b = new SecretStream(false)
const server = new RPCServer()
server.register('echo', async (params) => ({ echo: params.msg }))
server.handleConnection(a)
const client = new RPCClient(b)
pairSecretStreams(a, b)
await waitSecretStreamsConnected(a, b)
const res = await client.call('echo', { msg: 'p2p' })
t.is(res.echo, 'p2p')
server.close()
a.destroy()
b.destroy()
})
+29
View File
@@ -0,0 +1,29 @@
require('bare-process/global')
const test = require('brittle')
const SecretStream = require('@hyperswarm/secret-stream')
const { pairSecretStreams, waitSecretStreamsConnected } = require('../lib/pair.js')
const { RPCServer, RPCClient } = require('../index.js')
test('rpc: streaming call', async function (t) {
const a = new SecretStream(true)
const b = new SecretStream(false)
pairSecretStreams(a, b)
await waitSecretStreamsConnected(a, b)
const server = new RPCServer()
server.register('nums', async function * () {
yield { n: 1 }
yield { n: 2 }
})
server.handleConnection(a)
const client = new RPCClient(b)
const stream = await client.callStream('nums', {})
const out = []
for await (const chunk of stream) out.push(chunk.n)
t.alike(out, [1, 2])
server.close()
a.destroy()
b.destroy()
})