Updates
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## v0.1.0
|
||||
|
||||
- Initial release: Lowest-latency peer selection by capability tag.
|
||||
## v0.3.0
|
||||
|
||||
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||
@@ -0,0 +1,9 @@
|
||||
# hyper-p2p-anycast-selector
|
||||
|
||||
Lowest-latency peer selection by capability tag.
|
||||
|
||||
**Protocol:** `anycast-selector/v1`
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
# API: hyper-p2p-anycast-selector
|
||||
|
||||
**Protocol:** `anycast-selector/v1`
|
||||
|
||||
**Export:** `HyperP2PAnycastSelector`
|
||||
|
||||
## Constructor
|
||||
|
||||
```js
|
||||
const mod = new HyperP2PAnycastSelector(opts)
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `topic` | `string` \| `Buffer` | `null` | Hyperswarm topic; enables P2P when set |
|
||||
| `keyPair` | `KeyPair` | random | Ed25519 key pair (`hypercore-crypto`) |
|
||||
| `enableBackgroundTimers` | `boolean` | `false` | Periodic timers (keep false in unit tests) |
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Returns | Notes |
|
||||
|--------|---------|-------|
|
||||
| `close(...)` | See source | — |
|
||||
| `getStats(...)` | See source | — |
|
||||
| `ready(...)` | See source | — |
|
||||
| `registerCapability(...)` | See source | — |
|
||||
| `resolve(...)` | See source | — |
|
||||
| `updateLatency(...)` | See source | — |
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description |
|
||||
|-------|-------------|
|
||||
| `closed` | Module-specific |
|
||||
| `resolved` | Module-specific |
|
||||
|
||||
## Metrics
|
||||
|
||||
Call `getStats()` when implemented for counters (Wave 6 network modules always expose stats).
|
||||
|
||||
## P2P
|
||||
|
||||
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux `anycast-selector/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm test
|
||||
```
|
||||
|
||||
Integration: [`../../real_tests/integration/anycast-selector-two-node.js`](../../real_tests/integration/anycast-selector-two-node.js)
|
||||
@@ -0,0 +1,22 @@
|
||||
# Architecture: hyper-p2p-anycast-selector
|
||||
|
||||
Lowest-latency capability routing for Bare/Pear P2P overlays.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
App[Application] --> Mod[HyperP2PAnycastSelector]
|
||||
Mod --> Mux[Protomux anycast-selector/v1]
|
||||
Mux --> Swarm[Hyperswarm topic]
|
||||
```
|
||||
|
||||
## Wire messages (gossip)
|
||||
|
||||
JSON envelopes via `gossipSend` when connected. Message `type` fields are module-specific; see `index.js` `onmessage` handler.
|
||||
|
||||
## Composition (Wave 6)
|
||||
|
||||
See [`../_shared/WAVE6_NETWORK_STACK.md`](../_shared/WAVE6_NETWORK_STACK.md) for pairing with network-stack modules.
|
||||
|
||||
## State
|
||||
|
||||
In-memory maps/arrays; merged from remote gossip when `topic` is configured.
|
||||
@@ -0,0 +1,11 @@
|
||||
require('bare-process/global')
|
||||
const { HyperP2PAnycastSelector } = require('..')
|
||||
|
||||
async function main () {
|
||||
const m = new HyperP2PAnycastSelector()
|
||||
m.registerCapability('svc'); m.resolve('svc')
|
||||
console.log('ok', m.getStats())
|
||||
await m.close()
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
@@ -0,0 +1,73 @@
|
||||
require('bare-process/global')
|
||||
const EventEmitter = require('bare-events')
|
||||
const b4a = require('b4a')
|
||||
const { initModuleSwarm, gossipSend } = require('../_shared/p2p-bare.js')
|
||||
const PROTOCOL = 'anycast-selector/v1'
|
||||
|
||||
class HyperP2PAnycastSelector extends EventEmitter {
|
||||
constructor (opts = {}) {
|
||||
super()
|
||||
this.topic = opts.topic || null
|
||||
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
|
||||
this.peerId = b4a.toString(this.keyPair.publicKey, 'hex')
|
||||
this._tags = new Map()
|
||||
this._latency = new Map()
|
||||
this._stats = { resolves: 0 }
|
||||
this.swarm = null
|
||||
this._peerMsgs = null
|
||||
}
|
||||
|
||||
registerCapability (tag, peerId = null) {
|
||||
if (!tag) throw new Error('tag required')
|
||||
const pid = peerId || this.peerId
|
||||
if (!this._tags.has(tag)) this._tags.set(tag, new Set())
|
||||
this._tags.get(tag).add(pid)
|
||||
if (this._peerMsgs) gossipSend(this, { type: 'cap-register', tag, peerId: pid })
|
||||
return true
|
||||
}
|
||||
|
||||
updateLatency (peerId, ms) {
|
||||
if (!peerId) throw new Error('peerId required')
|
||||
if (ms < 0) throw new Error('latency must be non-negative')
|
||||
this._latency.set(peerId, ms)
|
||||
if (this._peerMsgs) gossipSend(this, { type: 'latency', peerId, ms })
|
||||
}
|
||||
|
||||
resolve (tag) {
|
||||
if (!tag) throw new Error('tag required')
|
||||
const peers = this._tags.get(tag)
|
||||
if (!peers || peers.size === 0) return null
|
||||
let best = null
|
||||
let bestMs = Infinity
|
||||
for (const p of peers) {
|
||||
const ms = this._latency.get(p) ?? 9999
|
||||
if (ms < bestMs) { bestMs = ms; best = p }
|
||||
}
|
||||
this._stats.resolves++
|
||||
this.emit('resolved', { tag, peerId: best, rttMs: bestMs })
|
||||
return best
|
||||
}
|
||||
|
||||
getStats () { return { ...this._stats, tags: this._tags.size } }
|
||||
|
||||
async ready () {
|
||||
if (this.swarm || !this.topic) return this
|
||||
await initModuleSwarm(this, {
|
||||
keyPair: this.keyPair, topic: this.topic, protocol: PROTOCOL,
|
||||
onmessage: (data) => {
|
||||
if (data?.type === 'cap-register') {
|
||||
if (!this._tags.has(data.tag)) this._tags.set(data.tag, new Set())
|
||||
this._tags.get(data.tag).add(data.peerId)
|
||||
} else if (data?.type === 'latency') this._latency.set(data.peerId, data.ms)
|
||||
}
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
async close () {
|
||||
if (this.swarm) await this.swarm.destroy().catch(() => {})
|
||||
this.swarm = null
|
||||
this.emit('closed')
|
||||
}
|
||||
}
|
||||
module.exports = { HyperP2PAnycastSelector, PROTOCOL }
|
||||
+1790
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "hyper-p2p-anycast-selector",
|
||||
"version": "0.3.0",
|
||||
"description": "Lowest-latency peer selection by capability tag.",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": { "test": "brittle-bare test/test.js" },
|
||||
"dependencies": {
|
||||
"bare-events": "^2.8.0",
|
||||
"bare-process": "^4.4.0",
|
||||
"bare-timers": "^2.0.0",
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.0.0",
|
||||
"protomux": "^3.0.0",
|
||||
"compact-encoding": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": { "hyperswarm": "^4.0.0", "bare": ">=1.0.0" },
|
||||
"devDependencies": { "brittle": "^3.0.0" },
|
||||
"imports": {
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"events": { "bare": "bare-events", "default": "events" },
|
||||
"timers": { "bare": "bare-timers", "default": "timers" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
require('bare-process/global')
|
||||
const test = require('brittle')
|
||||
const { HyperP2PAnycastSelector } = require('../index.js')
|
||||
|
||||
test('hyper-p2p-anycast-selector: basic operation', async (t) => {
|
||||
const m = new HyperP2PAnycastSelector()
|
||||
m.registerCapability('compute', 'peer-a')
|
||||
m.registerCapability('compute', 'peer-b')
|
||||
m.updateLatency('peer-a', 10)
|
||||
m.updateLatency('peer-b', 50)
|
||||
t.is(m.resolve('compute'), 'peer-a')
|
||||
await m.close()
|
||||
})
|
||||
|
||||
test('hyper-p2p-anycast-selector: validation', async (t) => {
|
||||
const m = new HyperP2PAnycastSelector()
|
||||
try {
|
||||
m.resolve(null)
|
||||
t.fail('expected throw')
|
||||
} catch (e) {
|
||||
t.ok(e instanceof Error)
|
||||
}
|
||||
await m.close()
|
||||
})
|
||||
|
||||
test('hyper-p2p-anycast-selector: getStats', async (t) => {
|
||||
const m = new HyperP2PAnycastSelector()
|
||||
const s = m.getStats()
|
||||
t.ok(s)
|
||||
await m.close()
|
||||
})
|
||||
|
||||
test('hyper-p2p-anycast-selector: close idempotent', async (t) => {
|
||||
const m = new HyperP2PAnycastSelector()
|
||||
await m.close()
|
||||
await m.close()
|
||||
t.pass()
|
||||
})
|
||||
Reference in New Issue
Block a user