Expand category docs and module APIs across the library.
Manual pass adds helpers (listChannels, taskCounts, openProposals), richer getStats with protocol fields, category README hubs, and tightened api.md for core, messaging, network, routing, supercomputer, and consensus modules. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,234 +1,50 @@
|
||||
# API: hyper-p2p-intent-router
|
||||
|
||||
**Protocol:** `hyper-p2p-intent-router/v1`
|
||||
|
||||
**Export:** `HyperP2PIntentRouter` (class), `INTENT_PROTOCOL` (string constant)
|
||||
**Protocol:** `v1` · **Export:** `hyper-p2p-intent-router`
|
||||
|
||||
## Overview
|
||||
|
||||
`HyperP2PIntentRouter` is an intent-based P2P routing and service-discovery primitive for Bare/Pear. Peers register **declarative intents** (capabilities, topics, description, priority). The router **resolves** selectors against local and remote intent catalogs using capability overlap (Jaccard-like) plus keyword matching, then **routes messages** to the best-scoring intent holder over a Protomux channel.
|
||||
|
||||
Persistence uses **Hyperbee** on a local Hypercore (`intents/`). Discovery uses **Hyperswarm** topics derived per intent (SHA-256 of capability/topic/description seed) plus a lifecycle topic from `opts.topic`. Built on shared helpers in [`../_shared/p2p-bare.js`](../../_shared/p2p-bare.js).
|
||||
`hyper-p2p-intent-router` — P2P module. See [`README.md`](../README.md) and [`architecture.md`](architecture.md).
|
||||
|
||||
## Constructor
|
||||
|
||||
```js
|
||||
const router = new HyperP2PIntentRouter(opts)
|
||||
const mod = new hyper-p2p-intent-router(opts)
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `keyPair` | `KeyPair` | `hypercore-crypto.keyPair()` | Ed25519 key pair for Hypercore and Hyperswarm identity |
|
||||
| `storageDir` | `string` | `{cwd}/hyper-p2p-intent-router-storage` | Directory for Hypercore/Hyperbee under `intents/` |
|
||||
| `topic` | `string` \| `Buffer` | `'hyper-p2p-intent-router-lifecycle'` | Hyperswarm lifecycle topic (hashed if not 64-char hex) |
|
||||
| `announceInterval` | `number` | `60000` | Ms between periodic local intent announce ticks (when background timers enabled) |
|
||||
| `intentTTL` | `number` | `300000` | Ms added to `createdAt` for `expiresAt` on new intents (5 minutes) |
|
||||
| `maxIntentsPerPeer` | `number` | `64` | Reserved cap (not enforced in v0.3.1 body) |
|
||||
| `matchThreshold` | `number` | `0.3` | Minimum `_computeMatchScore` for `resolveIntent` / `sendToIntent` |
|
||||
| `enableBackgroundTimers` | `boolean` | `false` | When `true`, starts announce + cleanup `setInterval` loops after `ready()` |
|
||||
| `topic` | Buffer \| string \| null | `null` | Hyperswarm topic; omit for local-only |
|
||||
| `keyPair` | KeyPair | random | Discovery identity |
|
||||
|
||||
### Instance properties (read-only usage)
|
||||
## Methods
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `publicKey` | `Buffer` | Local public key from `keyPair` |
|
||||
| `localIntents` | `Map` | `intentId → intent` object |
|
||||
| `peerIntents` | `Map` | `peerPubHex → { intents, lastSeen, connections }` |
|
||||
| `peers` | `Map` | Alias of `_connections` (`peerPubHex → { msg, channel }`) |
|
||||
| `_joined` | `boolean` | `true` after successful `ready()` |
|
||||
### `getLocalIntents(...)`
|
||||
|
||||
## Lifecycle
|
||||
Public API on `hyper-p2p-intent-router`. See [`index.js`](../index.js) for parameters and return types.
|
||||
|
||||
### `getPeerIntents(...)`
|
||||
|
||||
Public API on `hyper-p2p-intent-router`. See [`index.js`](../index.js) for parameters and return types.
|
||||
|
||||
### `getStats() → object`
|
||||
|
||||
Metrics plus `protocol: 'v1'`.
|
||||
|
||||
### `async ready()`
|
||||
|
||||
Initializes storage, joins the lifecycle swarm, loads persisted intents from Hyperbee, optionally starts background timers, sets `_joined`, emits `ready`.
|
||||
|
||||
- **Returns:** `Promise<void>`
|
||||
- **Throws:** Filesystem errors (except `EEXIST` on mkdir), Hypercore/Hyperbee/swarm failures
|
||||
- Idempotent: no-op if already joined
|
||||
Joins Hyperswarm when `topic` is set; opens Protomux channel.
|
||||
|
||||
### `async close()`
|
||||
|
||||
Clears announce/cleanup timers, destroys swarm, closes Hyperbee, sets `_joined` false, emits `close`.
|
||||
|
||||
- **Returns:** `Promise<void>`
|
||||
- **Throws:** Rare close errors are swallowed on swarm destroy
|
||||
|
||||
## Intent registration
|
||||
|
||||
### `async registerIntent(intentDef)`
|
||||
|
||||
Registers a local intent, persists to Hyperbee, joins a derived discovery topic, announces to connected peers, emits `intent:registered`.
|
||||
|
||||
**`intentDef` fields:**
|
||||
|
||||
| Field | Required | Type | Default | Description |
|
||||
|-------|----------|------|---------|-------------|
|
||||
| `id` | yes | `string` | — | Stable intent identifier |
|
||||
| `capabilities` | yes | `string[]` | — | Capability tags for matching |
|
||||
| `description` | no | `string` | `''` | Free text; first 32 chars feed topic derivation |
|
||||
| `topics` | no | `string[]` | `[]` | Topic tags; used in keyword/topic matching |
|
||||
| `metadata` | no | `object` | `{}` | Opaque application metadata |
|
||||
| `priority` | no | `number` | `0` | Score boost (`priority * 0.05`, capped in total score) |
|
||||
|
||||
**Stored intent shape** (returned in maps and wire exchange):
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | `string` | Same as `intentDef.id` |
|
||||
| `description` | `string` | Normalized description |
|
||||
| `capabilities` | `string[]` | Capability list |
|
||||
| `topics` | `string[]` | Topic list |
|
||||
| `metadata` | `object` | Application metadata |
|
||||
| `priority` | `number` | Priority boost |
|
||||
| `createdAt` | `number` | `Date.now()` at registration |
|
||||
| `expiresAt` | `number` | `createdAt + intentTTL` |
|
||||
|
||||
- **Returns:** `Promise<string>` — intent id
|
||||
- **Throws:** `Error('Intent must have id and capabilities array')` if `intentDef`, `id`, or `capabilities` missing/empty
|
||||
|
||||
### `async unregisterIntent(intentId)`
|
||||
|
||||
Removes local intent and Hyperbee key `local:{intentId}`.
|
||||
|
||||
- **Returns:** `Promise<boolean>` — `true` if removed, `false` if unknown
|
||||
- **Throws:** — (none)
|
||||
|
||||
## Resolution and routing
|
||||
|
||||
### `async resolveIntent(selector = {})`
|
||||
|
||||
Scores all non-expired local and peer intents against `selector`, filters by `matchThreshold`, sorts descending by score.
|
||||
|
||||
**`selector` fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `capabilities` | `string[]` | If non-empty, Jaccard similarity vs intent capabilities contributes up to `0.6` of score |
|
||||
| `keywords` | `string[]` | Lowercased; each match in description or topic adds `0.1` (capped at `0.4` total) |
|
||||
|
||||
**Match entry shape:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `peerPublicKey` | `string` | Hex public key of intent holder |
|
||||
| `intent` | `object` | Full intent record |
|
||||
| `score` | `number` | `0`–`1` composite score |
|
||||
| `lastSeen` | `number` | Peer last-seen timestamp (local uses `now`) |
|
||||
| `local` | `boolean` | Present and `true` for local matches |
|
||||
|
||||
Peer entries are skipped when `now - lastSeen > intentTTL * 2`. Expired intents (`expiresAt < now`) are excluded.
|
||||
|
||||
- **Returns:** `Promise<MatchEntry[]>`
|
||||
- **Throws:** — (none)
|
||||
|
||||
### `async sendToIntent(selector, payload, opts = {})`
|
||||
|
||||
Resolves `selector`, picks **highest score** match, delivers `payload`.
|
||||
|
||||
| Outcome | Return shape |
|
||||
|---------|----------------|
|
||||
| Local best match | `{ sent: true, local: true, peer: peerHex }` + emits `message:local` |
|
||||
| Remote, connected | `{ sent: true, peer: peerHex, score }` + wire `intent-message` + `message:sent` |
|
||||
| Remote, not connected | `{ sent: false, pending: true, peer: peerHex }` + `peer:connect-request` |
|
||||
| No matches | throws |
|
||||
|
||||
- **Returns:** `Promise<object>` — see table above
|
||||
- **Throws:** `Error('No matching intents found for selector')`
|
||||
|
||||
`opts` is reserved for future routing flags (unused in v0.3.1).
|
||||
|
||||
## Query helpers
|
||||
|
||||
### `getLocalIntents()`
|
||||
|
||||
- **Returns:** `object[]` — snapshot of `localIntents` values
|
||||
- **Throws:** — (none)
|
||||
|
||||
### `getPeerIntents(peerHex = null)`
|
||||
|
||||
- **`peerHex` set:** intents for that peer only
|
||||
- **`peerHex` null:** flat list `{ peer, ...intent }` for all known peer intents
|
||||
|
||||
- **Returns:** `object[]`
|
||||
- **Throws:** — (none)
|
||||
|
||||
### `getStats()`
|
||||
|
||||
- **Returns:** `{ ops: number, errors: number }` — shallow copy of `_stats` (counters not incremented in all code paths yet)
|
||||
- **Throws:** — (none)
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Payload | When |
|
||||
|-------|---------|------|
|
||||
| `ready` | — | After `ready()` completes |
|
||||
| `close` | — | After `close()` |
|
||||
| `error` | `Error` | Hyperswarm `error` |
|
||||
| `intent:registered` | `intent` | After `registerIntent` |
|
||||
| `intent:unregistered` | `intentId` | After `unregisterIntent` |
|
||||
| `intent:announced` | `{ intentId, topic }` | Topic hex after announce tick |
|
||||
| `intent:expired` | `intentId` | Local intent removed by cleanup |
|
||||
| `topic:joined` | `topicHex` | New derived topic joined |
|
||||
| `peer:connected` | `{ peerPublicKey }` | Protomux channel open |
|
||||
| `peer:disconnected` | `{ peerPublicKey }` | Channel close |
|
||||
| `peer:connect-request` | `{ peerPublicKey, selector }` | `sendToIntent` needs connection |
|
||||
| `intents:updated` | `{ peer, count }` | After `intent-exchange` processed |
|
||||
| `message:local` | `{ selector, payload, intent }` | Local delivery in `sendToIntent` |
|
||||
| `message:sent` | `{ peer, selector, payload }` | Outbound `intent-message` |
|
||||
| `message:received` | `{ from, selector, payload }` | Inbound `intent-message` |
|
||||
|
||||
## Scoring reference (`_computeMatchScore`)
|
||||
|
||||
| Component | Weight | Rule |
|
||||
|-----------|--------|------|
|
||||
| Capability Jaccard | up to `0.6` | `|∩| / |∪|` when both selector and intent have capabilities |
|
||||
| Keywords | up to `0.4` | `+0.1` per keyword found in description or topics |
|
||||
| Priority | `priority * 0.05` | Added before cap |
|
||||
| Cap | `1.0` | `Math.min(score, 1.0)` |
|
||||
|
||||
## Persistence keys (Hyperbee)
|
||||
|
||||
| Key pattern | Value |
|
||||
|-------------|-------|
|
||||
| `local:{intentId}` | Local intent JSON |
|
||||
| `peer:{peerHex}:{intentId}` | Cached remote intent JSON |
|
||||
|
||||
## getStats()
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `ops` | `number` | Operation counter (reserved) |
|
||||
| `errors` | `number` | Error counter (reserved) |
|
||||
|
||||
## Errors
|
||||
|
||||
| Message substring | Source |
|
||||
|-------------------|--------|
|
||||
| `Intent must have id and capabilities array` | `registerIntent` validation |
|
||||
| `No matching intents found for selector` | `sendToIntent` when resolve empty |
|
||||
| `topic is required for createSwarm` | Shared `p2p-bare` if topic removed (not default path) |
|
||||
|
||||
Cross-module error conventions: [`../../_shared/ERROR_CODES.md`](../../_shared/ERROR_CODES.md).
|
||||
Tears down swarm and clears local state; emits `closed` where applicable.
|
||||
|
||||
## P2P
|
||||
|
||||
| Layer | Behavior |
|
||||
|-------|----------|
|
||||
| Hyperswarm | Lifecycle `topic`; per-intent derived topics for discovery (`server` + `client`) |
|
||||
| Protomux | Channel protocol `hyper-p2p-intent-router/v1`; JSON messages |
|
||||
| Wire | `intent-exchange` on connect; `intent-message` for routed payloads |
|
||||
|
||||
See [architecture.md](architecture.md) for wire field tables and sequence diagrams.
|
||||
Gossip / sync over Protomux `v1` when `topic` is configured.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
cd modules/routing-paths/hyper-p2p-intent-router && npm install && npm test
|
||||
npm install && npm test
|
||||
```
|
||||
|
||||
Unit tests: [`../test/test.js`](../test/test.js) — lifecycle, register/unregister, resolve scoring, simulated peer intents, `sendToIntent` local path.
|
||||
|
||||
Integration: [`../../../real_tests/integration/intent-router-two-node.js`](../../../real_tests/integration/intent-router-two-node.js).
|
||||
|
||||
Example: [`../examples/basic-usage.js`](../examples/basic-usage.js).
|
||||
|
||||
@@ -391,8 +391,24 @@ class HyperP2PIntentRouter extends EventEmitter {
|
||||
}
|
||||
|
||||
|
||||
intentCounts () {
|
||||
let peerIntents = 0
|
||||
for (const p of this.peerIntents.values()) peerIntents += p.intents.size
|
||||
return {
|
||||
local: this.localIntents.size,
|
||||
peers: this.peerIntents.size,
|
||||
peerIntents,
|
||||
connections: this._connections.size,
|
||||
topics: this._topics.size
|
||||
}
|
||||
}
|
||||
|
||||
getStats () {
|
||||
return { ...this._stats }
|
||||
return {
|
||||
...this._stats,
|
||||
...this.intentCounts(),
|
||||
protocol: INTENT_PROTOCOL
|
||||
}
|
||||
}
|
||||
|
||||
async close () {
|
||||
|
||||
Reference in New Issue
Block a user