This commit is contained in:
Raven Scott
2026-05-20 23:36:32 -04:00
parent a020270cb1
commit be94546cd3
218 changed files with 9189 additions and 3078 deletions
@@ -1,36 +1,90 @@
# hyper-p2p-capability-discovery
Gossip capability tags and metadata for capability-based peer selection.
Gossip capability names and metadata so peers can find who offers a given service on a shared Hyperswarm topic.
**Category:** network-discovery
**Composes with:** hyper-p2p-capabilities
**Protocol:** `capability-discovery/v1`
**Category:** network-discovery · **Protocol:** `capability-discovery/v1` · **Exports:** `HyperP2PCapabilityDiscovery`, `PROTOCOL`
## When to use
Mesh apps where peers advertise services (storage, relay, compute) and consumers query by capability name.
Capability-based peer selection (relay, storage, compute tags) on one swarm topic.
## When not to use
Fixed peer lists or DHT-only discovery with no shared gossip topic.
Fixed peer lists or discovery without gossip.
## Quick start
```js
const { HyperP2PCapabilityDiscovery } = require('hyper-p2p-capability-discovery')
const mod = new HyperP2PCapabilityDiscovery({ topic: process.argv[2] })
await mod.ready()
mod.registerCapability('relay', { version: 1 })
console.log(mod.findByCapability('relay'))
await mod.close()
const cap = new HyperP2PCapabilityDiscovery({ topic: 'discover' })
await cap.ready()
cap.registerCapability('storage', { region: 'eu' })
console.log(cap.findByCapability('storage'))
await cap.close()
```
## Docs
- [docs/api.md](docs/api.md) — constructor, methods, events, errors
- [docs/architecture.md](docs/architecture.md) — wire types, state, composition
- [docs/api.md](docs/api.md) · [docs/architecture.md](docs/architecture.md)
## Test
```bash
npm install && npm test
```
## API
| Member | Description |
|--------|-------------|
| `new HyperP2PCapabilityDiscovery(opts?)` | `topic`, `keyPair` (default random). Sets `peerHex` from public key. |
| `registerCapability(name, meta?)` | Local register + gossip. Returns `{ name, meta, peer, at }`. |
| `findByCapability(name)` | All entries for `name` (local + remote). |
| `listCapabilities()` | Distinct capability names. |
| `getStats()` | `{ registered, gossipIn, gossipOut, capabilities, peers, protocol }`. |
| `ready()` / `close()` | Join swarm when `topic` set; clear maps and destroy swarm. |
**Events:** `register`, `remote-register`, `closed`. Gossip only after `ready()` with `topic`.
## Architecture
```
Peer A --cap-register--> swarm peers --merge by (name, peer)--> in-memory Map<name, Map<peerHex, entry>>
```
State is `_byName`: capability → peer hex → entry. Remote updates apply last-write-wins on `at`. Local `registerCapability` always wins for this peers hex bucket. Works without `topic` (local-only; no gossip).
## Wire table
| `type` | Fields | Direction |
|--------|--------|-----------|
| `cap-register` | `name`, `meta`, `peer`, `at` | Any peer → all |
## Errors
- Empty `name` on `registerCapability` / `findByCapability` (`assertNonEmpty`).
- Without `topic`, `ready()` is a no-op for swarm; gossip requires joined peers.
## Composition
- Hyperswarm topic is the discovery plane; capability name is the app-level index.
- Pair with `hyper-p2p-seeder-registry` when capabilities imply content topics.
- Local-only mode: omit `topic` and use `registerCapability` / `findByCapability` in-process.
## Example
```js
const { HyperP2PCapabilityDiscovery } = require('hyper-p2p-capability-discovery')
const disc = new HyperP2PCapabilityDiscovery({ topic: process.argv[2] })
disc.on('remote-register', (e) => console.log('remote', e.name, e.peer))
await disc.ready()
disc.registerCapability('relay', { version: 1 })
console.log(disc.findByCapability('relay'))
console.log(disc.getStats())
await disc.close()
```
## Test
@@ -1,45 +1,91 @@
# API: hyper-p2p-capability-discovery
**Protocol:** `capability-discovery/v1`
**Protocol:** `capability-discovery/v1` · **Export:** `HyperP2PCapabilityDiscovery`, `PROTOCOL`
**Export:** `HyperP2PCapabilityDiscovery`
## Overview
Gossip registry mapping capability names to peers offering them. Supports multi-peer entries per capability with LWW merge on `at` timestamp.
## Constructor
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `topic` | string \| Buffer | null | Hyperswarm join topic |
| `keyPair` | KeyPair | random | Ed25519 identity |
| `topic` | `string` \| `Buffer` | `null` | Swarm topic |
| `keyPair` | `KeyPair` | random | `peerHex` identity |
## Methods
### `registerCapability(name, meta = {})`
### `registerCapability(name, meta?)`
- **Returns:** `{ name, meta, peer, at }`
- **Throws:** `ValidationError` if `name` is empty
- **Returns:** `{ name, meta, peer: peerHex, at }`
- **Throws:** `assertNonEmpty` on `name`
- **Gossip:** `{ type: 'cap-register', name, meta, peer, at }`
- **Emits:** `register`
### `findByCapability(name)`
- **Returns:** `Array<entry>` for that capability (all peers)
- **Returns:** array of entries for capability
### `listCapabilities()`
- **Returns:** `string[]` capability names with at least one peer
- **Returns:** array of capability name strings
### `getStats()` / `ready()` / `close()`
### `ready()` / `close()`
Standard lifecycle; see architecture doc.
Clears `_byName` on close.
## Events
| Event | Payload |
|-------|---------|
| `register` | local entry |
| `remote-register` | remote entry |
| `remote-register` | merged remote entry |
| `closed` | — |
## getStats()
`registered`, `gossipIn`, `gossipOut`, `capabilities` (name count), `peers` (total entries), `protocol`.
## Wire
| type | fields | behavior |
|------|--------|----------|
| `cap-register` | `name`, `meta`, `peer`, `at` | LWW per `(name, peer)` bucket |
## Errors
`assertNonEmpty` on `name` in public methods.
See [`../../_shared/ERROR_CODES.md`](../../_shared/ERROR_CODES.md).
## P2P
`_onGossip` resolves `peer` from message or `peerInfo.publicKey`.
## Testing
```bash
npm install && npm test
cd modules/network-discovery/hyper-p2p-capability-discovery && npm test
```
## Composition
`hyper-p2p-topic-announcer`, `hyper-p2p-seeder-registry`, `hyper-p2p-discovery-health`.
## Example
See [`examples/basic.js`](../examples/basic.js).
## Remote merge rules
- Incoming `cap-register` ignored without `name`
- Peer id from `data.peer` or gossip sender `publicKey`
- Replace bucket entry when `at >= prev.at`
## Lifecycle
Call `ready()` before expecting gossip side effects. `close()` destroys swarm and clears capability map.
## See also
[`docs/architecture.md`](architecture.md), [`../../MODULE_CATEGORIES.md`](../../MODULE_CATEGORIES.md).
@@ -1,18 +1,17 @@
# Architecture: hyper-p2p-capability-discovery
**Category:** network-discovery
**Protocol:** `capability-discovery/v1` · **Category:** network-discovery
## Wire messages
| type | fields | direction | behavior |
|------|--------|-----------|----------|
| `cap-register` | `name`, `meta`, `peer`, `at` | gossip | Upsert peer under capability name |
| type | direction | fields | behavior |
|------|-----------|--------|----------|
| `cap-register` | gossip | `name`, `meta`, `peer`, `at` | LWW merge into `_byName[name][peer]`; emit `remote-register` |
## State model
- `_byName`: `Map<name, Map<peerHex, entry>>`
- One entry per peer per capability (LWW on `at`)
`_byName`: `Map<name, Map<peerHex, entry>>`.
## Composition
`initModuleSwarm` + `gossipSend` from `../../_shared/p2p-bare.js`.
`hyper-p2p-topic-announcer`, `hyper-p2p-seeder-registry`.
@@ -1,39 +1,88 @@
# hyper-p2p-discovery-health
Gossip peer health scores and RTT hints for discovery overlays.
Gossip peer health reports (ok, score, RTT) for discovery-time filtering on a shared topic.
**Category:** network-discovery
**Composes with:** hyper-p2p-link-probe
**Protocol:** `discovery-health/v1`
**Category:** network-discovery · **Protocol:** `discovery-health/v1` · **Exports:** `HyperP2PDiscoveryHealth`, `PROTOCOL`
## When to use
Discovery layers that rank or filter peers by health before routing or replication.
Aggregate liveness and quality signals across a mesh before routing or bootstrap.
## When not to use
Full observability stacks (use hyper-p2p-health-probe or metrics modules instead).
Centralized monitoring only (no P2P gossip needed).
## Quick start
```js
const { HyperP2PDiscoveryHealth } = require('hyper-p2p-discovery-health')
const mod = new HyperP2PDiscoveryHealth({ topic: process.argv[2] })
await mod.ready()
mod.reportPeer('peer-a', { ok: true, rttMs: 12 })
console.log(mod.healthyPeers())
await mod.close()
const health = new HyperP2PDiscoveryHealth({ topic: 'discover' })
await health.ready()
health.reportPeer('abc123', { ok: true, score: 0.9, rttMs: 42 })
console.log(health.healthyPeers())
await health.close()
```
## Docs
- [docs/api.md](docs/api.md) — constructor, methods, events, errors
- [docs/architecture.md](docs/architecture.md) — wire types, state, composition
- [docs/api.md](docs/api.md) · [docs/architecture.md](docs/architecture.md)
## Test
```bash
npm install && npm test
npm test
```
## API
| Member | Description |
|--------|-------------|
| `new HyperP2PDiscoveryHealth(opts?)` | `topic`, `keyPair`. |
| `reportPeer(peerId, status?)` | `{ ok, score, rttMs, ...status }`. Default `ok: true`, `score: 1`. Gossips `peer-health`. |
| `get(peerId)` | Entry or `null`. |
| `healthyPeers()` | Entries with `ok !== false`. |
| `getStats()` | `{ reports, gossipIn, gossipOut, peers, healthy, protocol }`. |
| `ready()` / `close()` | Swarm lifecycle. |
**Events:** `report`, `remote-report`, `closed`.
## Architecture
```
reportPeer --> _peers Map(peerId -> entry) --> gossip peer-health --> peers merge by peerId + at (LWW)
```
`healthyPeers()` is a view filter, not persisted separately. Without `topic`, reports stay local.
## Wire table
| `type` | Fields | Direction |
|--------|--------|-----------|
| `peer-health` | `entry`: `{ peerId, ok, score, rttMs, at, ... }` | Any → all |
## Errors
- Empty `peerId` on `reportPeer` / `get`.
- Extra fields on `status` are merged into the stored entry (spread after defaults).
## Composition
- Feed reports from probes or `hyper-p2p-udx-metrics` RTT estimates.
- Filter bootstrap candidates with `healthyPeers()` before `hyper-p2p-peer-bootstrap-store` use.
- Omit `topic` for single-node health caches without gossip.
## Example
```js
const { HyperP2PDiscoveryHealth } = require('hyper-p2p-discovery-health')
const health = new HyperP2PDiscoveryHealth({ topic: process.argv[2] })
await health.ready()
health.reportPeer('abc123', { ok: true, score: 0.9, rttMs: 42 })
health.reportPeer('dead-peer', { ok: false, score: 0 })
console.log(health.healthyPeers())
console.log(health.getStats())
await health.close()
```
@@ -1,15 +1,28 @@
# API: hyper-p2p-discovery-health
**Protocol:** `discovery-health/v1`
**Protocol:** `discovery-health/v1` · **Export:** `HyperP2PDiscoveryHealth`, `PROTOCOL`
**Export:** `HyperP2PDiscoveryHealth`
## Overview
Gossip peer health reports with ok/score/rtt and arbitrary status fields. Remote reports merge LWW by `entry.at`.
## Constructor
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `topic` | `string` \| `Buffer` | `null` | Swarm topic |
| `keyPair` | `KeyPair` | random | Local identity |
## Methods
### `reportPeer(peerId, status = {})`
### `reportPeer(peerId, status?)`
- **Returns:** entry with `ok`, `score`, `rttMs`, `at`, plus spread `status`
- **Throws:** `ValidationError` if `peerId` is empty
Builds entry: `ok` defaults true unless `status.ok === false`; `score` defaults to `0` if not ok else `1` unless numeric `status.score`; `rttMs` from status or `0`.
- **Returns:** entry
- **Throws:** `assertNonEmpty` on `peerId`
- **Gossip:** `{ type: 'peer-health', entry }`
- **Emits:** `report`
### `get(peerId)`
@@ -17,18 +30,63 @@
### `healthyPeers()`
- **Returns:** entries where `ok !== false`
- **Returns:** entries where `ok === true`
### `ready()` / `close()`
Standard P2P lifecycle.
## Events
| Event | Payload |
|-------|---------|
| `report` | local entry |
| `remote-report` | remote entry |
| `remote-report` | merged entry |
| `closed` | — |
## getStats()
`reports`, `gossipIn`, `gossipOut`, `peers`, `healthy`, `protocol`.
## Wire
| type | fields | behavior |
|------|--------|----------|
| `peer-health` | `entry` (full object) | LWW on `entry.peerId` by `entry.at` |
## Errors
`assertNonEmpty` on `peerId`.
See [`../../_shared/ERROR_CODES.md`](../../_shared/ERROR_CODES.md).
## P2P
Spreads health scores for discovery ranking alongside capability and seeder modules.
## Testing
```bash
npm install && npm test
cd modules/network-discovery/hyper-p2p-discovery-health && npm test
```
## Composition
`hyper-p2p-capability-discovery`, `hyper-p2p-seeder-registry`, `hyper-p2p-load-spread`.
## Example
See [`examples/basic.js`](../examples/basic.js).
## Remote merge rules
- Only `peer-health` with `entry` object applied
- Replace when `entry.at >= prev.at` for same `peerId`
## Lifecycle
`healthyPeers()` is derived filter — not persisted separately from `_peers`.
## See also
[`docs/architecture.md`](architecture.md), [`../../MODULE_CATEGORIES.md`](../../MODULE_CATEGORIES.md).
@@ -1,17 +1,17 @@
# Architecture: hyper-p2p-discovery-health
**Category:** network-discovery
**Protocol:** `discovery-health/v1` · **Category:** network-discovery
## Wire messages
| type | fields | direction | behavior |
|------|--------|-----------|----------|
| `peer-health` | `entry` (`peerId`, `ok`, `score`, `rttMs`, `at`, …) | gossip | LWW merge on `entry.at` |
| type | direction | fields | behavior |
|------|-----------|--------|----------|
| `peer-health` | gossip | `entry` (`peerId`, `ok`, `score`, `rttMs`, `at`, …) | LWW merge; emit `remote-report` |
## State model
- `_peers`: `Map<peerId, entry>`
`_peers`: Map peerId → health entry.
## Composition
Discovery overlay health scores; pair with hyper-p2p-link-probe for measurements.
`hyper-p2p-capability-discovery`, `hyper-p2p-seeder-registry`.
@@ -1,39 +1,85 @@
# hyper-p2p-peer-bootstrap-store
Gossip bootstrap hints (host, port, relays) keyed by peer id.
Gossip bootstrap hints (addresses, routes) keyed by peer id for mesh cold-start and reconnection.
**Category:** network-discovery
**Composes with:** hyper-p2p-gossip-mesh
**Protocol:** `peer-bootstrap-store/v1`
**Category:** network-discovery · **Protocol:** `peer-bootstrap-store/v1` · **Exports:** `HyperP2PPeerBootstrapStore`, `PROTOCOL`
## When to use
Apps that collect dial hints from peers and need them replicated across a discovery topic.
Peers share how to reach other peers (`hints` objects) on one discovery topic.
## When not to use
Centralized bootstrap servers only, or when hints must stay local (do not call `ready()`).
Static bootstrap lists baked into config only.
## Quick start
```js
const { HyperP2PPeerBootstrapStore } = require('hyper-p2p-peer-bootstrap-store')
const mod = new HyperP2PPeerBootstrapStore({ topic: process.argv[2] })
await mod.ready()
mod.addBootstrap('peer-1', { host: '127.0.0.1', port: 49737 })
console.log(mod.allBootstraps())
await mod.close()
const store = new HyperP2PPeerBootstrapStore({ topic: 'discover' })
await store.ready()
store.addBootstrap('peer-a', { host: '1.2.3.4', port: 49737 })
console.log(store.allBootstraps())
await store.close()
```
## Docs
- [docs/api.md](docs/api.md) — constructor, methods, events, errors
- [docs/architecture.md](docs/architecture.md) — wire types, state, composition
- [docs/api.md](docs/api.md) · [docs/architecture.md](docs/architecture.md)
## Test
```bash
npm install && npm test
npm test
```
## API
| Member | Description |
|--------|-------------|
| `new HyperP2PPeerBootstrapStore(opts?)` | `topic`, `keyPair`; `peerHex` = local public key hex. |
| `addBootstrap(peerId, hints?)` | Store + gossip. Returns `{ peerId, hints, from, at }`. |
| `getBootstrap(peerId)` | Entry or `null`. |
| `allBootstraps()` | All values. |
| `getStats()` | `{ added, gossipIn, gossipOut, bootstraps, protocol }`. |
| `ready()` / `close()` | Swarm lifecycle. |
**Events:** `add`, `remote-add`, `closed`.
## Architecture
```
addBootstrap --> _bootstraps Map(peerId) --> bootstrap-add gossip --> LWW on at per peerId
```
`from` records gossip source (hex public key). Remote `from` falls back to senders swarm public key when missing.
## Wire table
| `type` | Fields | Direction |
|--------|--------|-----------|
| `bootstrap-add` | `peerId`, `hints`, `from`, `at` | Any → all |
## Errors
- Empty `peerId` on `addBootstrap` / `getBootstrap`.
- `hints` defaults to `{}` when omitted.
## Composition
- Populate `hints` from DHT or relay modules (`hyper-p2p-dht-bootstrap-hint`, `hyper-p2p-blind-relay-bridge`).
- Consumers read `allBootstraps()` after `discovery-health` filtering.
- `from` helps trace which peer advertised a route.
## Example
```js
const { HyperP2PPeerBootstrapStore } = require('hyper-p2p-peer-bootstrap-store')
const store = new HyperP2PPeerBootstrapStore({ topic: process.argv[2] })
await store.ready()
store.addBootstrap('peer-a', { host: '1.2.3.4', port: 49737 })
console.log(store.allBootstraps())
await store.close()
```
@@ -1,14 +1,26 @@
# API: hyper-p2p-peer-bootstrap-store
**Protocol:** `peer-bootstrap-store/v1`
**Protocol:** `peer-bootstrap-store/v1` · **Export:** `HyperP2PPeerBootstrapStore`, `PROTOCOL`
**Export:** `HyperP2PPeerBootstrapStore`
## Overview
Stores bootstrap hint objects per `peerId` and gossips additions. Remote entries merge LWW on `at`.
## Constructor
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `topic` | `string` \| `Buffer` | `null` | Swarm topic |
| `keyPair` | `KeyPair` | random | `peerHex` |
## Methods
### `addBootstrap(peerId, hints = {})`
### `addBootstrap(peerId, hints?)`
- **Returns:** `{ peerId, hints, from, at }`
- **Returns:** `{ peerId, hints, from: peerHex, at }`
- **Throws:** `assertNonEmpty` on `peerId`
- **Gossip:** `{ type: 'bootstrap-add', peerId, hints, from, at }`
- **Emits:** `add`
### `getBootstrap(peerId)`
@@ -16,18 +28,63 @@
### `allBootstraps()`
- **Returns:** `Array<entry>`
- **Returns:** array of all entries
### `ready()` / `close()`
Clears store on close.
## Events
| Event | Payload |
|-------|---------|
| `add` | local entry |
| `remote-add` | remote entry |
| `remote-add` | merged entry |
| `closed` | — |
## getStats()
`added`, `gossipIn`, `gossipOut`, `bootstraps`, `protocol`.
## Wire
| type | fields | behavior |
|------|--------|----------|
| `bootstrap-add` | `peerId`, `hints`, `from`, `at` | LWW per `peerId` |
## Errors
`assertNonEmpty` on `peerId`.
See [`../../_shared/ERROR_CODES.md`](../../_shared/ERROR_CODES.md).
## P2P
`from` falls back to gossip sender public key hex.
## Testing
```bash
npm install && npm test
cd modules/network-discovery/hyper-p2p-peer-bootstrap-store && npm test
```
## Composition
`hyper-p2p-dht-bootstrap-hint`, `hyper-bare-bundle-bridge`, `hyper-p2p-seeder-registry`.
## Example
See [`examples/basic.js`](../examples/basic.js).
## Remote merge rules
- `bootstrap-add` LWW on `peerId` by `at`
- `from` resolved from message or gossip `peerInfo.publicKey`
## Lifecycle
`allBootstraps()` returns live snapshot; not cached copies.
## See also
[`docs/architecture.md`](architecture.md), [`../../MODULE_CATEGORIES.md`](../../MODULE_CATEGORIES.md).
@@ -1,17 +1,17 @@
# Architecture: hyper-p2p-peer-bootstrap-store
**Category:** network-discovery
**Protocol:** `peer-bootstrap-store/v1` · **Category:** network-discovery
## Wire messages
| type | fields | direction | behavior |
|------|--------|-----------|----------|
| `bootstrap-add` | `peerId`, `hints`, `from`, `at` | gossip | LWW upsert per `peerId` |
| type | direction | fields | behavior |
|------|-----------|--------|----------|
| `bootstrap-add` | gossip | `peerId`, `hints`, `from`, `at` | LWW merge; emit `remote-add` |
## State model
- `_bootstraps`: `Map<peerId, entry>`
`_bootstraps`: Map peerId entry.
## Composition
Replicates dial hints across a gossip mesh topic.
`hyper-p2p-dht-bootstrap-hint`, `hyper-bare-bundle-bridge`.
@@ -1,39 +1,86 @@
# hyper-p2p-seeder-registry
Gossip which peers seed which logical topics.
Register and lookup seeders per logical `topicId` over gossip on a shared swarm topic.
**Category:** network-discovery
**Composes with:** hyper-p2p-gossip-mesh
**Protocol:** `seeder-registry/v1`
**Category:** network-discovery · **Protocol:** `seeder-registry/v1` · **Exports:** `HyperP2PSeederRegistry`, `PROTOCOL`
## When to use
Content or topic meshes where downloaders need to find seeders for a topic id on a shared swarm.
Content or topic seeding: many peers announce who seeds which `topicId`.
## When not to use
Single-seeder deployments with no peer discovery requirement.
Single fixed seeder with no mesh discovery.
## Quick start
```js
const { HyperP2PSeederRegistry } = require('hyper-p2p-seeder-registry')
const mod = new HyperP2PSeederRegistry({ topic: process.argv[2] })
await mod.ready()
mod.registerSeeder('topic-1', mod.peerHex, { slots: 8 })
console.log(mod.lookup('topic-1'))
await mod.close()
const reg = new HyperP2PSeederRegistry({ topic: 'discover' })
await reg.ready()
reg.registerSeeder('my-feed', reg.peerHex, { role: 'primary' })
console.log(reg.lookup('my-feed'))
await reg.close()
```
## Docs
- [docs/api.md](docs/api.md) — constructor, methods, events, errors
- [docs/architecture.md](docs/architecture.md) — wire types, state, composition
- [docs/api.md](docs/api.md) · [docs/architecture.md](docs/architecture.md)
## Test
```bash
npm install && npm test
npm test
```
## API
| Member | Description |
|--------|-------------|
| `new HyperP2PSeederRegistry(opts?)` | `topic`, `keyPair`; `peerHex` local id. |
| `registerSeeder(topicId, peerId, meta?)` | Upsert in topic bucket + gossip. |
| `lookup(topicId)` | All seeders for topic. |
| `listSeeders()` | Flat list across topics. |
| `getStats()` | `{ registered, gossipIn, gossipOut, topics, seeders, protocol }`. |
| `ready()` / `close()` | Swarm lifecycle. |
**Events:** `register`, `remote-register`, `closed`.
## Architecture
```
_byTopic: Map<topicId, Map<peerId, entry>>
registerSeeder --> bucket[topicId][peerId] --> seeder-register gossip --> LWW on at
```
Same `peerId` under one `topicId` is one slot; newer `at` replaces metadata.
## Wire table
| `type` | Fields | Direction |
|--------|--------|-----------|
| `seeder-register` | `topicId`, `peerId`, `meta`, `from`, `at` | Any → all |
## Errors
- Empty `topicId` or `peerId` on `registerSeeder` / `lookup`.
- `lookup` returns `[]` when topic unknown (not an error).
## Composition
- Align `topicId` with `hyper-p2p-topic-announcer` announcements.
- Use `hyper-p2p-capability-discovery` tag `seed` for capability-based lookup.
- Same swarm `topic` as other discovery modules for one mesh view.
## Example
```js
const { HyperP2PSeederRegistry } = require('hyper-p2p-seeder-registry')
const reg = new HyperP2PSeederRegistry({ topic: process.argv[2] })
await reg.ready()
reg.registerSeeder('my-feed', reg.peerHex, { role: 'primary' })
console.log(reg.lookup('my-feed'))
await reg.close()
```
@@ -1,37 +1,90 @@
# API: hyper-p2p-seeder-registry
**Protocol:** `seeder-registry/v1`
**Protocol:** `seeder-registry/v1` · **Export:** `HyperP2PSeederRegistry`, `PROTOCOL`
**Export:** `HyperP2PSeederRegistry`
## Overview
Registers seeders per `topicId` with metadata and gossips registrations. Lookup returns all seeders known for a topic; LWW per `(topicId, peerId)` on merge.
## Constructor
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `topic` | `string` \| `Buffer` | `null` | Swarm topic |
| `keyPair` | `KeyPair` | random | `peerHex` |
## Methods
### `registerSeeder(topicId, peerId, meta = {})`
### `registerSeeder(topicId, peerId, meta?)`
- **Returns:** `{ topicId, peerId, meta, from, at }`
- **Returns:** `{ topicId, peerId, meta, from: peerHex, at }`
- **Throws:** `assertNonEmpty` on `topicId`, `peerId`
- **Gossip:** `{ type: 'seeder-register', topicId, peerId, meta, from, at }`
- **Emits:** `register`
### `lookup(topicId)`
- **Returns:** seeders for that topic
- **Returns:** array of seeder entries
### `listSeeders()`
- **Returns:** all seeder entries across topics
- **Returns:** flat array across all topics
## Properties
### `ready()` / `close()`
- `peerHex` — local public key hex (from `keyPair`)
Clears `_byTopic` on close.
## Events
| Event | Payload |
|-------|---------|
| `register` | local entry |
| `remote-register` | remote entry |
| `remote-register` | merged entry |
| `closed` | — |
## getStats()
`registered`, `gossipIn`, `gossipOut`, `topics`, `seeders`, `protocol`.
## Wire
| type | fields | behavior |
|------|--------|----------|
| `seeder-register` | `topicId`, `peerId`, `meta`, `from`, `at` | LWW per peer in topic bucket |
## Errors
`assertNonEmpty` on ids.
See [`../../_shared/ERROR_CODES.md`](../../_shared/ERROR_CODES.md).
## P2P
Internal `_topicBucket(topicId)``Map<peerId, entry>`.
## Testing
```bash
npm install && npm test
cd modules/network-discovery/hyper-p2p-seeder-registry && npm test
```
## Composition
`hyper-p2p-topic-announcer`, `hyper-p2p-capability-discovery`, `hyper-p2p-discovery-health`.
## Example
See [`examples/basic.js`](../examples/basic.js).
## Remote merge rules
- `seeder-register` LWW per `(topicId, peerId)` on `at`
- `from` falls back to gossip sender key hex
## Lifecycle
`listSeeders()` flattens all topic buckets for export/debug.
## See also
[`docs/architecture.md`](architecture.md), [`../../MODULE_CATEGORIES.md`](../../MODULE_CATEGORIES.md).
@@ -1,17 +1,17 @@
# Architecture: hyper-p2p-seeder-registry
**Category:** network-discovery
**Protocol:** `seeder-registry/v1` · **Category:** network-discovery
## Wire messages
| type | fields | direction | behavior |
|------|--------|-----------|----------|
| `seeder-register` | `topicId`, `peerId`, `meta`, `from`, `at` | gossip | Upsert seeder per topic + peer |
| type | direction | fields | behavior |
|------|-----------|--------|----------|
| `seeder-register` | gossip | `topicId`, `peerId`, `meta`, `from`, `at` | LWW into `_byTopic[topicId][peerId]`; emit `remote-register` |
## State model
- `_byTopic`: `Map<topicId, Map<peerId, entry>>`
`_byTopic`: Map topicId Map peerId entry.
## Composition
Seeder directory for topic-based content meshes.
`hyper-p2p-topic-announcer`, `hyper-p2p-capability-discovery`.
@@ -1,41 +1,89 @@
# hyper-p2p-topic-announcer
Gossip topic announcements over a shared Hyperswarm topic.
Announce and revoke logical topics (with metadata) on a gossip mesh; track remote announcements.
**Category:** network-discovery
**Composes with:** hyper-p2p-presence
**Protocol:** `topic-announcer/v1`
**Category:** network-discovery · **Protocol:** `topic-announcer/v1` · **Exports:** `HyperP2PTopicAnnouncer`, `PROTOCOL`
## When to use
Apps that publish which logical topics they host and need peers to discover those announcements on a shared swarm topic.
Peers advertise interest in named `topicId`s (channels, feeds) without joining each topics swarm yet.
## When not to use
Static topic lists baked into config, or apps with no P2P topic (skip `ready()` and use local-only announce/revoke).
When topic membership is implicit from DHT only.
## Quick start
```js
const { HyperP2PTopicAnnouncer } = require('hyper-p2p-topic-announcer')
const topic = process.argv[2]
const mod = new HyperP2PTopicAnnouncer({ topic })
await mod.ready()
mod.announce('my-app-v1', { region: 'eu' })
console.log(mod.list())
await mod.close()
const ann = new HyperP2PTopicAnnouncer({ topic: 'discover' })
await ann.ready()
ann.announce('channel-1', { priority: 1 })
console.log(ann.list())
await ann.close()
```
## Docs
- [docs/api.md](docs/api.md) — constructor, methods, events, errors
- [docs/architecture.md](docs/architecture.md) — wire types, state, composition
- [../../_shared/PRODUCTION.md](../../_shared/PRODUCTION.md) — production checklist
- [docs/api.md](docs/api.md) · [docs/architecture.md](docs/architecture.md)
## Test
```bash
npm install && npm test
npm test
```
## API
| Member | Description |
|--------|-------------|
| `new HyperP2PTopicAnnouncer(opts?)` | `topic` (swarm), `keyPair`; `peerHex`. |
| `announce(topicId, meta?)` | Set local + gossip `announce`. |
| `revoke(topicId)` | Delete if held; gossip `revoke`. Returns boolean. |
| `list()` / `get(topicId)` | All entries / one. |
| `getStats()` | `{ announced, revoked, gossipIn, gossipOut, topics, protocol }`. |
| `ready()` / `close()` | Swarm lifecycle. |
**Events:** `announce`, `revoke`, `remote-announce`, `remote-revoke`, `closed`.
## Architecture
```
_topics Map(topicId -> { topicId, meta, peer, at })
announce: LWW by at; revoke: delete if cur.peer matches revoker peer (or no cur)
```
Revoke only removes when the revoking `peer` matches the announcer (or entry absent).
## Wire table
| `type` | Fields | Direction |
|--------|--------|-----------|
| `announce` | `topicId`, `meta`, `peer`, `at` | Any → all |
| `revoke` | `topicId`, `peer`, `at` | Any → all |
## Errors
- Empty `topicId` on `announce`, `revoke`, `get`.
- `revoke` returns `false` when topic was not locally announced.
## Composition
- `topicId` is logical; constructor `topic` is the Hyperswarm discovery topic.
- Downstream: join per-topic swarms using announced ids + `hyper-p2p-seeder-registry`.
- Listen to `remote-announce` to build subscription UI or routing tables.
## Example
```js
const { HyperP2PTopicAnnouncer } = require('hyper-p2p-topic-announcer')
const ann = new HyperP2PTopicAnnouncer({ topic: process.argv[2] })
ann.on('remote-announce', (e) => console.log('seen', e.topicId))
await ann.ready()
ann.announce('channel-1', { priority: 1 })
console.log(ann.list())
ann.revoke('channel-1')
await ann.close()
```
@@ -1,59 +1,83 @@
# API: hyper-p2p-topic-announcer
**Protocol:** `topic-announcer/v1`
**Protocol:** `topic-announcer/v1` · **Export:** `HyperP2PTopicAnnouncer`, `PROTOCOL`
**Export:** `HyperP2PTopicAnnouncer`
## Overview
Logical topic announcements over a gossip mesh: peers publish interest in named `topicId` values with metadata before joining dedicated swarms.
## Constructor
```js
const mod = new HyperP2PTopicAnnouncer(opts)
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `topic` | string \| Buffer | null | Hyperswarm join topic |
| `keyPair` | KeyPair | random | Ed25519 identity |
| `topic` | `string` \| `Buffer` | `null` | Hyperswarm mesh for announcer gossip |
| `keyPair` | `KeyPair` | random | Local peer identity (`peerHex`) |
## Methods
### `announce(topicId, meta = {})`
### `announce(topicId, meta?)`
Registers or updates a local announcement. Gossips when swarm is active.
Registers or updates announcement (LWW by `at` on merge).
- **Returns:** entry `{ topicId, meta, peer, at }`
- **Throws:** `ValidationError` if `topicId` is empty
- **Returns:** `{ topicId, meta, peer, at }`
- **Throws:** `assertNonEmpty` on `topicId`
- **Gossip:** `{ type: 'announce', topicId, meta, peer, at }`
### `revoke(topicId)`
Removes a local announcement and gossips revoke.
Deletes local entry and gossips revoke if held.
- **Returns:** `boolean` — whether an entry existed
- **Returns:** `boolean`
- **Gossip:** `{ type: 'revoke', topicId, peer, at }`
### `list()`
### `list()` / `get(topicId)`
- **Returns:** `Array<entry>` — all known announcements
Snapshot of all announcements or single lookup.
### `get(topicId)`
### `ready()` / `close()`
- **Returns:** entry or `null`
`initModuleSwarm` when `topic` set. `close` clears map and destroys swarm.
### `getStats()` / `ready()` / `close()`
### `getStats()`
See architecture doc.
`announced`, `revoked`, `gossipIn`, `gossipOut`, `topics`, `protocol`.
## Remote merge rules
- `announce`: accept if newer `at`
- `revoke`: delete if revoking `peer` matches holder or no holder
## Events
| Event | Payload |
|-------|---------|
| `announce` | local entry |
| `revoke` | `{ topicId, peer }` |
| `remote-announce` | entry |
| `remote-revoke` | `{ topicId, peer }` |
| `closed` | — |
`announce`, `revoke`, `remote-announce`, `remote-revoke`, `closed`
## Wire
| type | fields |
|------|--------|
| `announce` | `topicId`, `meta`, `peer`, `at` |
| `revoke` | `topicId`, `peer`, `at` |
## Errors
Empty `topicId` rejected via `assertNonEmpty`.
## Testing
```bash
npm install && npm test
```
`npm test` — local announce/revoke and simulated gossip.
## P2P notes
Without `topic`, API is local-only; `ready()` is no-op for swarm.
## Integration
Compose with `hyper-p2p-capability-discovery` and `hyper-p2p-seeder-registry` for layered discovery.
## Example
See `examples/basic.js`.
## See also
[`docs/architecture.md`](architecture.md), [`../../_shared/ERROR_CODES.md`](../../_shared/ERROR_CODES.md).
@@ -1,27 +1,25 @@
# Architecture: hyper-p2p-topic-announcer
**Category:** network-discovery
**Protocol:** `topic-announcer/v1`
```mermaid
flowchart LR
App[Application] --> Mod[HyperP2PTopicAnnouncer]
Mod --> Mux[Protomux topic-announcer/v1]
Mux --> Swarm[Hyperswarm]
App --> Ann[TopicAnnouncer]
Ann --> Gossip[p2p-bare]
Gossip --> Peers
```
## Wire messages
| type | fields | direction | behavior |
|------|--------|-----------|----------|
| `announce` | `topicId`, `meta`, `peer`, `at` | gossip | Upsert if `at` is newer |
| `revoke` | `topicId`, `peer`, `at` | gossip | Delete if peer matches holder |
| type | direction | fields | behavior |
|------|-----------|--------|----------|
| `announce` | gossip | `topicId`, `meta`, `peer`, `at` | LWW merge into `_topics` |
| `revoke` | gossip | `topicId`, `peer`, `at` | Delete if peer matches holder |
## State model
## State
- `_topics`: `Map<topicId, entry>`
- LWW merge on `at` for remote announces
- `close()` clears map and destroys swarm
`_topics`: Map topicId entry. Stats track announce/revoke counts.
## Composition
Uses `../../_shared/p2p-bare.js` (`initModuleSwarm`, `gossipSend`). Composes with hyper-p2p-presence for peer identity context.
`hyper-p2p-dht-bootstrap-hint`, `hyper-p2p-topic-channel`.