Updates
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
# API: hyper-p2p-presence
|
||||
|
||||
**Protocol:** `hyper-p2p-presence/v1.1`
|
||||
|
||||
**Export:** `HyperP2PPresence` (also `PRESENCE_PROTOCOL`, `PROTOCOL`)
|
||||
|
||||
## Overview
|
||||
|
||||
`HyperP2PPresence` is a production-grade P2P presence and liveness manager for Bare/Pear. Peers join a shared Hyperswarm topic, exchange signed presence records over a dedicated Protomux channel, keep an in-memory peer map, and persist records in a local Hyperbee database backed by Hypercore. Optional background timers periodically rebroadcast local presence and mark stale peers offline.
|
||||
|
||||
The module extends `bare-events` `EventEmitter`. Call `ready()` before relying on swarm or storage; call `close()` for teardown.
|
||||
|
||||
## Constructor
|
||||
|
||||
```js
|
||||
const presence = new HyperP2PPresence(opts)
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `keyPair` | `KeyPair` | `hypercore-crypto.keyPair()` | Ed25519 key pair for Hypercore, signing, and swarm identity |
|
||||
| `topic` | `string` \| `Buffer` | `null` | Hyperswarm topic (64-char hex or string hashed via `topicToBuffer`); **required** for `ready()` swarm join |
|
||||
| `storageDir` | `string` | `path.join(process.cwd(), 'hyper-p2p-presence-storage')` | Root directory; Hypercore lives at `{storageDir}/presence` |
|
||||
| `announceInterval` | `number` | `30000` | Milliseconds between periodic self-announce when background timers are enabled (stored as `announceIntervalMs`) |
|
||||
| `expiry` | `number` | `120000` | Milliseconds until a peer record is considered stale if not refreshed (stored as `expiryMs`) |
|
||||
| `metadata` | `object` | `{}` | Arbitrary JSON-serializable metadata attached to local presence |
|
||||
| `enableBackgroundTimers` | `boolean` | `false` | When `true`, starts announce and cleanup `setInterval` loops after `ready()` |
|
||||
|
||||
## Methods
|
||||
|
||||
### `ready()`
|
||||
|
||||
Initializes Hyperbee storage, joins the Hyperswarm (when `topic` is set), registers self in `peers`, and optionally starts background timers.
|
||||
|
||||
- **Returns:** `Promise<void>`
|
||||
- **Throws:**
|
||||
- `Error: topic is required for presence swarm` — when `topic` is null/undefined during swarm init
|
||||
- Filesystem errors from `fs.mkdir` except `err.code === 'EEXIST'`
|
||||
|
||||
Idempotent: if already joined (`_joined === true`), resolves immediately without re-emitting `ready`.
|
||||
|
||||
Emits `ready` on first successful join.
|
||||
|
||||
### `close()`
|
||||
|
||||
Clears announce and cleanup timers, destroys the swarm, closes Hyperbee, sets `_joined` to `false`, emits `close`.
|
||||
|
||||
- **Returns:** `Promise<void>`
|
||||
- **Throws:** — (errors from `swarm.destroy()` / `bee.close()` are caught and ignored)
|
||||
|
||||
### `updateMetadata(newMetadata)`
|
||||
|
||||
Shallow-merges `newMetadata` into `this.metadata`, updates the local peer entry, persists to Hyperbee, emits `self-presence`.
|
||||
|
||||
- **Parameters:** `newMetadata` — `object` merged with `{ ...this.metadata, ...newMetadata }`
|
||||
- **Returns:** `Promise<void>`
|
||||
- **Throws:** — (Hyperbee `put` failures propagate)
|
||||
|
||||
Does not immediately send on the wire unless background announce runs or a peer connection triggers `_sendPresenceUpdate`.
|
||||
|
||||
### `getPeers(filter = {})`
|
||||
|
||||
Returns a snapshot array of presence records from the in-memory `peers` map.
|
||||
|
||||
- **Parameters:**
|
||||
- `filter.online` — `boolean` | `undefined`; when set, only peers matching that online flag
|
||||
- `filter.metadata` — `object`; every key must match `p.metadata[k]` exactly (shallow equality)
|
||||
- **Returns:** `Array<PresenceRecord>`
|
||||
- **Throws:** —
|
||||
|
||||
### `getSelf()`
|
||||
|
||||
- **Returns:** `PresenceRecord | null` — local peer entry keyed by hex public key, or `null` if missing
|
||||
- **Throws:** —
|
||||
|
||||
### `getStats()`
|
||||
|
||||
- **Returns:** `{ ops: number, errors: number }` — shallow copy of internal counters (initialized to `0`; not incremented by current implementation paths)
|
||||
- **Throws:** —
|
||||
|
||||
## Presence record shape
|
||||
|
||||
Objects in `peers`, events, and Hyperbee values share this structure:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `publicKey` | `string` | Hex-encoded Ed25519 public key |
|
||||
| `metadata` | `object` | Application metadata |
|
||||
| `lastSeen` | `number` | Unix ms timestamp of last update |
|
||||
| `online` | `boolean` | `true` while within expiry window |
|
||||
| `expiresAt` | `number` | Unix ms when record should go offline without refresh |
|
||||
| `signature` | `string \| null` | Base64 Ed25519 signature on wire updates (optional on stored records) |
|
||||
| `verified` | `boolean` | Whether incoming signature verified against `peerInfo.publicKey` (remote updates only) |
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Payload | When |
|
||||
|-------|---------|------|
|
||||
| `ready` | — | First successful `ready()` |
|
||||
| `close` | — | After `close()` |
|
||||
| `peer-connected` | `{ publicKey: string \| null }` | Protomux channel `onopen` for a remote peer |
|
||||
| `peer-disconnected` | `{ publicKey: string \| null }` | Channel `onclose` |
|
||||
| `peer-joined` | `PresenceRecord` | Remote presence first seen or transitions to online |
|
||||
| `peer-updated` | `PresenceRecord` | Remote presence refresh while already online |
|
||||
| `peer-left` | `PresenceRecord` | Cleanup timer marks peer offline (`expiresAt < now`) |
|
||||
| `presence-changed` | `PresenceRecord[]` | After one or more peers left in a cleanup tick |
|
||||
| `self-presence` | `PresenceRecord` | Background announce tick or `updateMetadata()` |
|
||||
|
||||
## getStats()
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|-------|------|---------|
|
||||
| `ops` | `number` | Reserved operation counter (default `0`) |
|
||||
| `errors` | `number` | Reserved error counter (default `0`) |
|
||||
|
||||
## Errors
|
||||
|
||||
Stable message substrings for tests and logging: see [`../_shared/ERROR_CODES.md`](../../_shared/ERROR_CODES.md).
|
||||
|
||||
Module-specific throws:
|
||||
|
||||
| Message | Source |
|
||||
|---------|--------|
|
||||
| `topic is required for presence swarm` | `_initSwarm()` when `topic` is missing |
|
||||
|
||||
Shared helper (`createSwarm` in `p2p-bare.js`) may throw `topic is required for createSwarm` if invoked without a topic (not reached when `topic` is set on the instance).
|
||||
|
||||
## P2P
|
||||
|
||||
1. `ready()` creates `{storageDir}/presence` Hypercore + Hyperbee (`keyEncoding: 'utf-8'`, `valueEncoding: 'json'`), loads persisted peers (marked `online: false` until a live announce).
|
||||
2. `createSwarm({ keyPair, topic })` joins Hyperswarm; `wireConnection` opens Protomux per socket.
|
||||
3. `protocolChannel(mux, { protocol: 'hyper-p2p-presence/v1.1', ... })` — compact-encoding JSON messages.
|
||||
4. On channel open, local node sends a signed `presence` envelope; `onmessage` handles remote `type === 'presence'`.
|
||||
5. Topic strings that match `/^[0-9a-f]{64}$/i` are used as raw 32-byte topics; other strings are hashed with `hypercore-crypto.hash`.
|
||||
|
||||
Signing uses Ed25519 via `hypercore-crypto.sign` / `verify` over a canonical JSON payload including `nonce` (16 random bytes, hex) for replay resistance. Wire `version` is `'1.1'`.
|
||||
|
||||
Enable `enableBackgroundTimers: true` in production so `announceInterval` broadcasts and `expiry`-driven cleanup run automatically.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
cd modules/core-infrastructure/hyper-p2p-presence
|
||||
npm install && npm test
|
||||
```
|
||||
|
||||
Tests cover lifecycle, metadata update, peer filters, signing fields, and close without topic. Use a unique `storageDir` per test run (see `test/test.js`).
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
bare examples/basic.js
|
||||
```
|
||||
|
||||
For two-node integration, run paired processes with the same `topic` and distinct `storageDir` / `keyPair` values.
|
||||
@@ -0,0 +1,136 @@
|
||||
# Architecture: hyper-p2p-presence
|
||||
|
||||
**Category:** Core infrastructure
|
||||
|
||||
**Protocol:** `hyper-p2p-presence/v1.1`
|
||||
|
||||
## Layer diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
App[Application] --> HP[HyperP2PPresence]
|
||||
HP --> EE[EventEmitter events]
|
||||
HP --> Map[peers Map in-memory]
|
||||
HP --> HB[Hyperbee on Hypercore]
|
||||
HB --> FS["{storageDir}/presence"]
|
||||
HP --> SW[Hyperswarm via createSwarm]
|
||||
SW --> MUX[Protomux per connection]
|
||||
MUX --> CH["Channel hyper-p2p-presence/v1.1"]
|
||||
CH --> Wire[JSON presence envelopes]
|
||||
```
|
||||
|
||||
## Sequence: ready → peer connect → announce
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App
|
||||
participant HP as HyperP2PPresence
|
||||
participant HB as Hyperbee
|
||||
participant SW as Hyperswarm
|
||||
participant Remote as Remote peer
|
||||
|
||||
App->>HP: ready()
|
||||
HP->>HB: mkdir storage, core.ready(), load stream
|
||||
HP->>SW: createSwarm(topic)
|
||||
HP->>HP: _ensureSelfRegistered()
|
||||
opt enableBackgroundTimers
|
||||
HP->>HP: _startAnnounceTimer()
|
||||
HP->>HP: _startCleanupTimer()
|
||||
end
|
||||
HP-->>App: emit ready
|
||||
|
||||
SW-->>HP: connection(socket, peerInfo, mux)
|
||||
HP->>HP: protocolChannel + _peerChannels.set
|
||||
HP-->>App: peer-connected
|
||||
HP->>Remote: send presence envelope
|
||||
Remote->>HP: presence envelope
|
||||
HP->>HP: verify signature, peers.set
|
||||
HP->>HB: bee.put(pubKeyHex, record)
|
||||
HP-->>App: peer-joined or peer-updated
|
||||
|
||||
loop every announceIntervalMs
|
||||
HP->>Remote: _broadcastPresence via _peerChannels
|
||||
HP->>HB: bee.put(self)
|
||||
HP-->>App: self-presence
|
||||
end
|
||||
|
||||
loop every min(expiryMs/2, 30000)
|
||||
HP->>HP: mark expired online=false
|
||||
HP-->>App: peer-left, presence-changed
|
||||
end
|
||||
```
|
||||
|
||||
## Wire messages
|
||||
|
||||
All messages use Protomux `compact-encoding` JSON on protocol `hyper-p2p-presence/v1.1`.
|
||||
|
||||
| Envelope `type` | Fields | Direction | Behavior |
|
||||
|-----------------|--------|-----------|----------|
|
||||
| `presence` | See `data` below | bidirectional | Only `type === 'presence'` is handled in `onmessage`; other types ignored |
|
||||
|
||||
### `presence` → `data` object
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `publicKey` | `string` | yes | Hex public key of the announcing peer |
|
||||
| `metadata` | `object` | no | Defaults to `{}` |
|
||||
| `timestamp` | `number` | no | Unix ms; defaults to receive time if omitted |
|
||||
| `nonce` | `string` | no | 32-char hex replay nonce; generated on send (16 random bytes) |
|
||||
| `version` | `string` | no | `'1.1'` on outbound records |
|
||||
| `signature` | `string` | no | Base64 Ed25519 signature over canonical JSON |
|
||||
|
||||
**Signature payload** (UTF-8 JSON stringified, then signed):
|
||||
|
||||
| Field | Value on send |
|
||||
|-------|----------------|
|
||||
| `publicKey` | Local hex public key |
|
||||
| `metadata` | `this.metadata` |
|
||||
| `timestamp` | Send-time ms |
|
||||
| `nonce` | Fresh random hex |
|
||||
| `version` | `'1.1'` |
|
||||
|
||||
Verification uses `peerInfo.publicKey` from Hyperswarm; sets `verified: true/false` on the stored record. Invalid or missing signatures do not drop the update—they store `verified: false`.
|
||||
|
||||
Full wire envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "presence",
|
||||
"data": { "publicKey": "...", "metadata": {}, "timestamp": 0, "nonce": "...", "version": "1.1", "signature": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
## State model
|
||||
|
||||
| Structure | Key | Value / role |
|
||||
|-----------|-----|----------------|
|
||||
| `peers` | `publicKey` hex | `PresenceRecord` — authoritative in-memory view |
|
||||
| `_peerChannels` | `publicKey` hex | `{ msg }` — Protomux message handle for `_broadcastPresence` |
|
||||
| `bee` (Hyperbee) | `pubKeyHex` | Same record JSON persisted across restarts |
|
||||
| `_joined` | — | `boolean` — `ready()` completed |
|
||||
| `announceTimer` | — | `setInterval` every `announceIntervalMs` (only if `enableBackgroundTimers`) |
|
||||
| `cleanupTimer` | — | `setInterval` every `min(expiryMs / 2, 30000)` (only if `enableBackgroundTimers`) |
|
||||
|
||||
**Persistence path:** `{storageDir}/presence/` — Hypercore directory name constant `PRESENCE_DB_NAME = 'presence'`.
|
||||
|
||||
**Expiry logic:** On cleanup tick, if `presence.expiresAt < now` and `presence.online`, set `online: false`, emit `peer-left`. Refreshed remote announces reset `expiresAt` to `now + expiryMs`.
|
||||
|
||||
**Load behavior:** `_loadPersistedPeers()` hydrates `peers` from Hyperbee with `online: false` until a live wire update.
|
||||
|
||||
## Composition
|
||||
|
||||
| Peer module | Relationship |
|
||||
|-------------|--------------|
|
||||
| `hyper-p2p-rpc` | App RPC over established SecretStream / swarm sockets; presence supplies who is online and metadata |
|
||||
| `hyper-p2p-capabilities` | Tokens and delegation layered on identified peers |
|
||||
| `hyper-p2p-link-probe` | RTT matrix; pairs with presence for anycast (see Wave 6 stack) |
|
||||
|
||||
Typical stack: Hyperswarm underlay → **presence** (who is here) → `hyper-p2p-protocol-handshake` / **rpc** (what they speak).
|
||||
|
||||
See [`../_shared/WAVE6_NETWORK_STACK.md`](../../_shared/WAVE6_NETWORK_STACK.md) for Wave 6 layering (`link-probe` ↔ presence, connection-pool, overlay-topology).
|
||||
|
||||
## Operational notes
|
||||
|
||||
- Background timers are **off** by default (`enableBackgroundTimers: false`); tests and minimal examples omit them. Production multi-peer apps should enable them.
|
||||
- `_sendPresenceUpdate` runs on channel open and on announce ticks; metadata-only changes via `updateMetadata` update local state and Hyperbee but do not fan out until the next broadcast or new connection.
|
||||
- Swarm and storage failures during `close()` are swallowed to ensure shutdown completes.
|
||||
Reference in New Issue
Block a user