Updates
This commit is contained in:
@@ -15,7 +15,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
unit-all:
|
unit-all:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 75
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
@@ -26,12 +26,12 @@ jobs:
|
|||||||
- name: Install real_tests deps
|
- name: Install real_tests deps
|
||||||
working-directory: real_tests
|
working-directory: real_tests
|
||||||
run: npm install
|
run: npm install
|
||||||
- name: Run all unit tests (42 modules)
|
- name: Run all unit tests (52 modules)
|
||||||
run: chmod +x real_tests/run-all.sh && ./real_tests/run-all.sh
|
run: chmod +x real_tests/run-all.sh && ./real_tests/run-all.sh
|
||||||
|
|
||||||
integration:
|
integration:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 75
|
||||||
needs: unit-all
|
needs: unit-all
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ Demos: [`../examples/p2p-agent-workflow-demo/`](../examples/p2p-agent-workflow-d
|
|||||||
|
|
||||||
Demo: [`../examples/p2p-chaos-mesh-demo/`](../examples/p2p-chaos-mesh-demo/).
|
Demo: [`../examples/p2p-chaos-mesh-demo/`](../examples/p2p-chaos-mesh-demo/).
|
||||||
|
|
||||||
**Production-ready (Wave 5):** all 42 modules meet [`_shared/PRODUCTION.md`](_shared/PRODUCTION.md) — presence-tier `docs/api.md`, mermaid architecture, ≥3 unit tests, clean app source (no pending markers).
|
**Production-ready (Wave 6):** all **52** modules meet [`_shared/PRODUCTION.md`](_shared/PRODUCTION.md) — presence-tier `docs/api.md`, mermaid architecture, ≥3 unit tests, clean app source (no pending markers). Network stack: [`_shared/WAVE6_NETWORK_STACK.md`](_shared/WAVE6_NETWORK_STACK.md).
|
||||||
|
|
||||||
### hyper-p2p-presence (v0.3.0)
|
### hyper-p2p-presence (v0.3.0)
|
||||||
- **Location**: `./hyper-p2p-presence/`
|
- **Location**: `./hyper-p2p-presence/`
|
||||||
|
|||||||
@@ -12,3 +12,17 @@ Modules throw plain `Error` instances (no error codes enum in v0.2). Typical mes
|
|||||||
| `Invalid parent capability` | capabilities delegation failure |
|
| `Invalid parent capability` | capabilities delegation failure |
|
||||||
|
|
||||||
Prefer stable message substrings when asserting in tests.
|
Prefer stable message substrings when asserting in tests.
|
||||||
|
|
||||||
|
## Wave 6 network modules
|
||||||
|
|
||||||
|
| Pattern | Meaning |
|
||||||
|
|---------|---------|
|
||||||
|
| `peerId required` | Missing peer identifier |
|
||||||
|
| `maxDegree exceeded` | overlay-topology neighbor cap |
|
||||||
|
| `maxConcurrent exceeded` | connection-pool cap |
|
||||||
|
| `pathCount must be` | multipath-fanout bounds |
|
||||||
|
| `priority must be control\|data\|bulk` | flow-shaper queue |
|
||||||
|
| `unknown circuit` / `hopPeers required` | circuit-loom cell errors |
|
||||||
|
| `unknown offer` | protocol-handshake |
|
||||||
|
| `bytes must be non-negative` | bandwidth-broker |
|
||||||
|
| `invalid rtt or loss` | congestion-signal |
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Wave 6 network stack composition
|
||||||
|
|
||||||
|
Layering for the 10 Wave 6 networking modules over existing underlay (Hyperswarm, `relay-tunnel`, `gossip-mesh`, `peer-scheduler`, `bucket-rate-limit`).
|
||||||
|
|
||||||
|
## Layer 1 — Session and topology
|
||||||
|
|
||||||
|
| Module | Role | Pairs with |
|
||||||
|
|--------|------|------------|
|
||||||
|
| `hyper-p2p-protocol-handshake` | Version/features before app mux | `capabilities`, `rpc` |
|
||||||
|
| `hyper-p2p-connection-pool` | Warm lanes, idle teardown | `topic-lease`, `peer-scheduler` |
|
||||||
|
| `hyper-p2p-overlay-topology` | Explicit neighbor graph | `peer-scheduler`, `gossip-mesh` |
|
||||||
|
|
||||||
|
## Layer 2 — Measurement and routing
|
||||||
|
|
||||||
|
| Module | Role | Pairs with |
|
||||||
|
|--------|------|------------|
|
||||||
|
| `hyper-p2p-link-probe` | RTT/jitter matrix | `presence` |
|
||||||
|
| `hyper-p2p-anycast-selector` | Lowest-latency capability pick | `link-probe`, `intent-router` |
|
||||||
|
| `hyper-p2p-congestion-signal` | Gossip rate hints | `bucket-rate-limit` |
|
||||||
|
|
||||||
|
## Layer 3 — Capacity and egress
|
||||||
|
|
||||||
|
| Module | Role | Pairs with |
|
||||||
|
|--------|------|------------|
|
||||||
|
| `hyper-p2p-bandwidth-broker` | Inter-peer send credits | `trust-graph`, `mycelium-pool` |
|
||||||
|
| `hyper-p2p-flow-shaper` | Priority QoS queues | `activity-queue`, `intent-router` |
|
||||||
|
| `hyper-p2p-multipath-fanout` | Striping + reassembly | `relay-tunnel`, `dedup-filter` |
|
||||||
|
|
||||||
|
## Layer 4 — Circuits
|
||||||
|
|
||||||
|
| Module | Role | Pairs with |
|
||||||
|
|--------|------|------------|
|
||||||
|
| `hyper-p2p-circuit-loom` | Multi-hop CREATE/EXTEND/RELAY | `relay-tunnel`, `overlay-topology` |
|
||||||
|
|
||||||
|
Hyperswarm provides discovery and single-hop connectivity only; `circuit-loom` does not replace NAT punch.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
HS[Hyperswarm underlay]
|
||||||
|
RT[relay-tunnel]
|
||||||
|
GM[gossip-mesh]
|
||||||
|
HS --> RT
|
||||||
|
HS --> GM
|
||||||
|
PH[protocol-handshake] --> CP[connection-pool]
|
||||||
|
CP --> OT[overlay-topology]
|
||||||
|
LP[link-probe] --> AS[anycast-selector]
|
||||||
|
AS --> CS[congestion-signal]
|
||||||
|
BB[bandwidth-broker] --> FS[flow-shaper]
|
||||||
|
FS --> MF[multipath-fanout]
|
||||||
|
OT --> CL[circuit-loom]
|
||||||
|
RT --> CL
|
||||||
|
```
|
||||||
@@ -11,3 +11,6 @@
|
|||||||
## v0.2.1
|
## v0.2.1
|
||||||
|
|
||||||
- Production docs, input validation, third test, integration notes.
|
- Production docs, input validation, third test, integration notes.
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||||
|
|||||||
@@ -18,15 +18,35 @@ const mod = new HyperP2PActivityQueue(opts)
|
|||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
See [`index.js`](../index.js) for the full method list. Core operations implement **distributed work queue with priority, DLQ, and optional vector-clock ordering**.
|
| Method | Returns | Notes |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| `ack(...)` | See source | — |
|
||||||
|
| `claim(...)` | See source | — |
|
||||||
|
| `close(...)` | See source | — |
|
||||||
|
| `enqueue(...)` | See source | — |
|
||||||
|
| `getDeadLetters(...)` | See source | — |
|
||||||
|
| `getQueueDepth(...)` | See source | — |
|
||||||
|
| `nack(...)` | See source | — |
|
||||||
|
| `ready(...)` | See source | — |
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
The instance extends `EventEmitter`. Common events: `closed`, plus module-specific events documented in source.
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `ack` | Module-specific |
|
||||||
|
| `claim` | Module-specific |
|
||||||
|
| `closed` | Module-specific |
|
||||||
|
| `enqueue` | Module-specific |
|
||||||
|
| `nack` | Module-specific |
|
||||||
|
| `remote-claim` | Module-specific |
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Call `getStats()` when implemented for counters (Wave 6 network modules always expose stats).
|
||||||
|
|
||||||
## P2P
|
## P2P
|
||||||
|
|
||||||
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux channel `activity-queue/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux `activity-queue/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
# Architecture: hyper-p2p-activity-queue
|
# Architecture: hyper-p2p-activity-queue
|
||||||
|
|
||||||
|
Distributed work queue with priority and dlq for Bare/Pear P2P overlays.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart TB
|
||||||
App[Application] --> Mod[HyperP2PActivityQueue]
|
App[Application] --> Mod[HyperP2PActivityQueue]
|
||||||
Mod --> P2P[Protomux activity-queue/v1]
|
Mod --> Mux[Protomux activity-queue/v1]
|
||||||
P2P --> Swarm[Hyperswarm]
|
Mux --> Swarm[Hyperswarm topic]
|
||||||
```
|
```
|
||||||
|
|
||||||
Local state lives in memory maps/arrays; gossip merges remote updates when `topic` is configured.
|
## 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.
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-activity-queue",
|
"name": "hyper-p2p-activity-queue",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hyper-p2p-activity-queue",
|
"name": "hyper-p2p-activity-queue",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.6.7",
|
"b4a": "^1.6.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-activity-queue",
|
"name": "hyper-p2p-activity-queue",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"description": "Distributed activity queue with enqueue/claim/ack for Bare/Pear P2P.",
|
"description": "Distributed activity queue with enqueue/claim/ack for Bare/Pear P2P.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
|
|||||||
@@ -46,3 +46,22 @@ test('hyper-p2p-activity-queue: close without leak', async (t) => {
|
|||||||
await m.close()
|
await m.close()
|
||||||
t.pass()
|
t.pass()
|
||||||
})
|
})
|
||||||
|
test('hyper-p2p-activity-queue: validation rejects invalid input', async (t) => {
|
||||||
|
const m = new HyperP2PActivityQueue()
|
||||||
|
try {
|
||||||
|
if (typeof m.addNeighbor === 'function') m.addNeighbor(null)
|
||||||
|
else if (typeof m.buildCircuit === 'function') m.buildCircuit([])
|
||||||
|
else if (typeof m.grant === 'function') m.grant(null, -1)
|
||||||
|
else if (typeof m.enqueue === 'function') m.enqueue('bad', null)
|
||||||
|
else if (typeof m.reportSample === 'function') m.reportSample(null, -1, -1)
|
||||||
|
else if (typeof m.fanout === 'function') m.fanout(null, 0)
|
||||||
|
else if (typeof m.probe === 'function') m.probe(null)
|
||||||
|
else if (typeof m.resolve === 'function') m.resolve(null)
|
||||||
|
else if (typeof m.acquire === 'function') m.acquire(null)
|
||||||
|
else throw new Error('no validation hook')
|
||||||
|
t.fail('expected throw')
|
||||||
|
} catch (err) {
|
||||||
|
t.ok(err instanceof Error)
|
||||||
|
}
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|||||||
@@ -19,3 +19,6 @@
|
|||||||
## v0.2.0
|
## v0.2.0
|
||||||
|
|
||||||
- Production-grade docs, validation, and expanded tests.
|
- Production-grade docs, validation, and expanded tests.
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||||
|
|||||||
@@ -18,15 +18,37 @@ const mod = new HyperP2PAgentMemory(opts)
|
|||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
See [`index.js`](../index.js) for the full method list. Core operations implement **agent memory graph with P2P gossip**.
|
| Method | Returns | Notes |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| `attachP2P(...)` | See source | — |
|
||||||
|
| `close(...)` | See source | — |
|
||||||
|
| `getMetrics(...)` | See source | — |
|
||||||
|
| `pruneExpired(...)` | See source | — |
|
||||||
|
| `recall(...)` | See source | — |
|
||||||
|
| `receiveMemory(...)` | See source | — |
|
||||||
|
| `storeMemory(...)` | See source | — |
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
The instance extends `EventEmitter`. Common events: `closed`, plus module-specific events documented in source.
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `closed` | Module-specific |
|
||||||
|
| `invalid-memory-signature` | Module-specific |
|
||||||
|
| `memory-gossip` | Module-specific |
|
||||||
|
| `memory-received` | Module-specific |
|
||||||
|
| `memory-stored` | Module-specific |
|
||||||
|
| `p2p-attached` | Module-specific |
|
||||||
|
| `persist-error` | Module-specific |
|
||||||
|
| `pruned` | Module-specific |
|
||||||
|
| `recalled` | Module-specific |
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Call `getStats()` when implemented for counters (Wave 6 network modules always expose stats).
|
||||||
|
|
||||||
## P2P
|
## P2P
|
||||||
|
|
||||||
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux channel `unknown/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux `unknown/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
# Architecture: hyper-p2p-agent-memory
|
# Architecture: hyper-p2p-agent-memory
|
||||||
|
|
||||||
|
Agent memory graph with p2p gossip for Bare/Pear P2P overlays.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart TB
|
||||||
App[Application] --> Mod[HyperP2PAgentMemory]
|
App[Application] --> Mod[HyperP2PAgentMemory]
|
||||||
Mod --> P2P[Protomux unknown/v1]
|
Mod --> Mux[Protomux unknown/v1]
|
||||||
P2P --> Swarm[Hyperswarm]
|
Mux --> Swarm[Hyperswarm topic]
|
||||||
```
|
```
|
||||||
|
|
||||||
Local state lives in memory maps/arrays; gossip merges remote updates when `topic` is configured.
|
## 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.
|
||||||
|
|||||||
Generated
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-agent-memory",
|
"name": "hyper-p2p-agent-memory",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hyper-p2p-agent-memory",
|
"name": "hyper-p2p-agent-memory",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.6.7",
|
"b4a": "^1.6.7",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../hyper-p2p-vector-clock": {
|
"../hyper-p2p-vector-clock": {
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.6.7",
|
"b4a": "^1.6.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-agent-memory",
|
"name": "hyper-p2p-agent-memory",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"description": "A novel, production-grade persistent causal memory graph primitive for autonomous agents in Bare/Pear P2P applications. Provides episodic memory with time-ordered entries, semantic tagging and recall, associative links, full causal ordering via integrated vector clocks, Ed25519 cryptographic signing and verification for tamper-proof memories, Hyperbee-backed persistence, Hyperswarm topic derivation for P2P replication, Protomux streaming hooks, and advanced query capabilities (temporal range, tag-based, causal ancestry, keyword search). Enables building stateful AI agents, decentralized knowledge bases, personal data stores, and long-term agent memory with P2P synchronization and Byzantine-resilient integrity. First reusable dedicated agent memory module in the Holepunch/Bare ecosystem — never-before-seen primitive combining episodic+semantic memory, causality, and decentralized sync.",
|
"description": "A novel, production-grade persistent causal memory graph primitive for autonomous agents in Bare/Pear P2P applications. Provides episodic memory with time-ordered entries, semantic tagging and recall, associative links, full causal ordering via integrated vector clocks, Ed25519 cryptographic signing and verification for tamper-proof memories, Hyperbee-backed persistence, Hyperswarm topic derivation for P2P replication, Protomux streaming hooks, and advanced query capabilities (temporal range, tag-based, causal ancestry, keyword search). Enables building stateful AI agents, decentralized knowledge bases, personal data stores, and long-term agent memory with P2P synchronization and Byzantine-resilient integrity. First reusable dedicated agent memory module in the Holepunch/Bare ecosystem — never-before-seen primitive combining episodic+semantic memory, causality, and decentralized sync.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
|
|||||||
@@ -129,3 +129,22 @@ test('hyper-p2p-agent-memory: close without leak', async (t) => {
|
|||||||
await m.close()
|
await m.close()
|
||||||
t.pass()
|
t.pass()
|
||||||
})
|
})
|
||||||
|
test('hyper-p2p-agent-memory: validation rejects invalid input', async (t) => {
|
||||||
|
const m = new HyperP2PAgentMemory()
|
||||||
|
try {
|
||||||
|
if (typeof m.addNeighbor === 'function') m.addNeighbor(null)
|
||||||
|
else if (typeof m.buildCircuit === 'function') m.buildCircuit([])
|
||||||
|
else if (typeof m.grant === 'function') m.grant(null, -1)
|
||||||
|
else if (typeof m.enqueue === 'function') m.enqueue('bad', null)
|
||||||
|
else if (typeof m.reportSample === 'function') m.reportSample(null, -1, -1)
|
||||||
|
else if (typeof m.fanout === 'function') m.fanout(null, 0)
|
||||||
|
else if (typeof m.probe === 'function') m.probe(null)
|
||||||
|
else if (typeof m.resolve === 'function') m.resolve(null)
|
||||||
|
else if (typeof m.acquire === 'function') m.acquire(null)
|
||||||
|
else throw new Error('no validation hook')
|
||||||
|
t.fail('expected throw')
|
||||||
|
} catch (err) {
|
||||||
|
t.ok(err instanceof Error)
|
||||||
|
}
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
@@ -7,3 +7,6 @@
|
|||||||
## v0.2.0
|
## v0.2.0
|
||||||
|
|
||||||
- Production-grade docs, validation, and expanded tests.
|
- Production-grade docs, validation, and expanded tests.
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||||
|
|||||||
@@ -18,15 +18,29 @@ const mod = new HyperP2PAttestationChain(opts)
|
|||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
See [`index.js`](../index.js) for the full method list. Core operations implement **linked signed attestation chain**.
|
| Method | Returns | Notes |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| `append(...)` | See source | — |
|
||||||
|
| `close(...)` | See source | — |
|
||||||
|
| `head(...)` | See source | — |
|
||||||
|
| `ready(...)` | See source | — |
|
||||||
|
| `verify(...)` | See source | — |
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
The instance extends `EventEmitter`. Common events: `closed`, plus module-specific events documented in source.
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `append` | Module-specific |
|
||||||
|
| `closed` | Module-specific |
|
||||||
|
| `remote-append` | Module-specific |
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Call `getStats()` when implemented for counters (Wave 6 network modules always expose stats).
|
||||||
|
|
||||||
## P2P
|
## P2P
|
||||||
|
|
||||||
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux channel `attestation-chain/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux `attestation-chain/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
# Architecture: hyper-p2p-attestation-chain
|
# Architecture: hyper-p2p-attestation-chain
|
||||||
|
|
||||||
|
Linked signed attestation chain for Bare/Pear P2P overlays.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart TB
|
||||||
App[Application] --> Mod[HyperP2PAttestationChain]
|
App[Application] --> Mod[HyperP2PAttestationChain]
|
||||||
Mod --> P2P[Protomux attestation-chain/v1]
|
Mod --> Mux[Protomux attestation-chain/v1]
|
||||||
P2P --> Swarm[Hyperswarm]
|
Mux --> Swarm[Hyperswarm topic]
|
||||||
```
|
```
|
||||||
|
|
||||||
Local state lives in memory maps/arrays; gossip merges remote updates when `topic` is configured.
|
## 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.
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-attestation-chain",
|
"name": "hyper-p2p-attestation-chain",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hyper-p2p-attestation-chain",
|
"name": "hyper-p2p-attestation-chain",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.6.7",
|
"b4a": "^1.6.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-attestation-chain",
|
"name": "hyper-p2p-attestation-chain",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"description": "Ed25519 attestation chain for Bare/Pear P2P.",
|
"description": "Ed25519 attestation chain for Bare/Pear P2P.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
|
|||||||
@@ -21,3 +21,22 @@ test('hyper-p2p-attestation-chain: close without leak', async (t) => {
|
|||||||
await m.close()
|
await m.close()
|
||||||
t.pass()
|
t.pass()
|
||||||
})
|
})
|
||||||
|
test('hyper-p2p-attestation-chain: validation rejects invalid input', async (t) => {
|
||||||
|
const m = new HyperP2PAttestationChain()
|
||||||
|
try {
|
||||||
|
if (typeof m.addNeighbor === 'function') m.addNeighbor(null)
|
||||||
|
else if (typeof m.buildCircuit === 'function') m.buildCircuit([])
|
||||||
|
else if (typeof m.grant === 'function') m.grant(null, -1)
|
||||||
|
else if (typeof m.enqueue === 'function') m.enqueue('bad', null)
|
||||||
|
else if (typeof m.reportSample === 'function') m.reportSample(null, -1, -1)
|
||||||
|
else if (typeof m.fanout === 'function') m.fanout(null, 0)
|
||||||
|
else if (typeof m.probe === 'function') m.probe(null)
|
||||||
|
else if (typeof m.resolve === 'function') m.resolve(null)
|
||||||
|
else if (typeof m.acquire === 'function') m.acquire(null)
|
||||||
|
else throw new Error('no validation hook')
|
||||||
|
t.fail('expected throw')
|
||||||
|
} catch (err) {
|
||||||
|
t.ok(err instanceof Error)
|
||||||
|
}
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
- Initial release: Inter-peer bandwidth credit grant and consume.
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# hyper-p2p-bandwidth-broker
|
||||||
|
|
||||||
|
Inter-peer bandwidth credit grant and consume.
|
||||||
|
|
||||||
|
**Protocol:** `bandwidth-broker/v1`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# API: hyper-p2p-bandwidth-broker
|
||||||
|
|
||||||
|
**Protocol:** `bandwidth-broker/v1`
|
||||||
|
|
||||||
|
**Export:** `HyperP2PBandwidthBroker`
|
||||||
|
|
||||||
|
## Constructor
|
||||||
|
|
||||||
|
```js
|
||||||
|
const mod = new HyperP2PBandwidthBroker(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 |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| `balance(...)` | See source | — |
|
||||||
|
| `close(...)` | See source | — |
|
||||||
|
| `consume(...)` | See source | — |
|
||||||
|
| `getStats(...)` | See source | — |
|
||||||
|
| `grant(...)` | See source | — |
|
||||||
|
| `ready(...)` | See source | — |
|
||||||
|
| `request(...)` | See source | — |
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `closed` | Module-specific |
|
||||||
|
| `consume` | Module-specific |
|
||||||
|
| `grant` | Module-specific |
|
||||||
|
| `request` | 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 `bandwidth-broker/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Integration: [`../../real_tests/integration/bandwidth-broker-two-node.js`](../../real_tests/integration/bandwidth-broker-two-node.js)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Architecture: hyper-p2p-bandwidth-broker
|
||||||
|
|
||||||
|
Inter-peer bandwidth credits for Bare/Pear P2P overlays.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
App[Application] --> Mod[HyperP2PBandwidthBroker]
|
||||||
|
Mod --> Mux[Protomux bandwidth-broker/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 { HyperP2PBandwidthBroker } = require('..')
|
||||||
|
|
||||||
|
async function main () {
|
||||||
|
const m = new HyperP2PBandwidthBroker()
|
||||||
|
m.grant('peer', 500); m.consume('peer', 100)
|
||||||
|
console.log('ok', m.getStats())
|
||||||
|
await m.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
require('bare-process/global')
|
||||||
|
const EventEmitter = require('bare-events')
|
||||||
|
const { initModuleSwarm, gossipSend } = require('../_shared/p2p-bare.js')
|
||||||
|
const PROTOCOL = 'bandwidth-broker/v1'
|
||||||
|
|
||||||
|
class HyperP2PBandwidthBroker extends EventEmitter {
|
||||||
|
constructor (opts = {}) {
|
||||||
|
super()
|
||||||
|
this.topic = opts.topic || null
|
||||||
|
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
|
||||||
|
this._balances = new Map()
|
||||||
|
this._stats = { granted: 0, consumed: 0 }
|
||||||
|
this.swarm = null
|
||||||
|
this._peerMsgs = null
|
||||||
|
}
|
||||||
|
|
||||||
|
grant (peerId, bytes) {
|
||||||
|
if (!peerId) throw new Error('peerId required')
|
||||||
|
if (bytes == null || bytes < 0) throw new Error('bytes must be non-negative')
|
||||||
|
const b = (this._balances.get(peerId) || 0) + bytes
|
||||||
|
this._balances.set(peerId, b)
|
||||||
|
this._stats.granted += bytes
|
||||||
|
if (this._peerMsgs) gossipSend(this, { type: 'grant', peerId, bytes, balance: b })
|
||||||
|
this.emit('grant', { peerId, bytes, balance: b })
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
consume (peerId, bytes) {
|
||||||
|
if (!peerId) throw new Error('peerId required')
|
||||||
|
if (bytes == null || bytes < 0) throw new Error('bytes must be non-negative')
|
||||||
|
const cur = this._balances.get(peerId) || 0
|
||||||
|
if (cur < bytes) return false
|
||||||
|
const b = cur - bytes
|
||||||
|
this._balances.set(peerId, b)
|
||||||
|
this._stats.consumed += bytes
|
||||||
|
if (this._peerMsgs) gossipSend(this, { type: 'consume', peerId, bytes, balance: b })
|
||||||
|
this.emit('consume', { peerId, bytes, balance: b })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
balance (peerId) { return this._balances.get(peerId) || 0 }
|
||||||
|
|
||||||
|
request (peerId, bytes) {
|
||||||
|
if (!peerId || bytes < 0) throw new Error('invalid request')
|
||||||
|
if (this._peerMsgs) gossipSend(this, { type: 'request', peerId, bytes })
|
||||||
|
this.emit('request', { peerId, bytes })
|
||||||
|
return this.balance(peerId) >= bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
getStats () { return { ...this._stats, peers: this._balances.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 === 'grant') this._balances.set(data.peerId, data.balance)
|
||||||
|
else if (data?.type === 'consume') this._balances.set(data.peerId, data.balance)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
async close () {
|
||||||
|
if (this.swarm) await this.swarm.destroy().catch(() => {})
|
||||||
|
this.swarm = null
|
||||||
|
this._balances.clear()
|
||||||
|
this.emit('closed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = { HyperP2PBandwidthBroker, PROTOCOL }
|
||||||
+1790
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "hyper-p2p-bandwidth-broker",
|
||||||
|
"version": "0.3.0",
|
||||||
|
"description": "Inter-peer bandwidth credit grant and consume.",
|
||||||
|
"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,35 @@
|
|||||||
|
require('bare-process/global')
|
||||||
|
const test = require('brittle')
|
||||||
|
const { HyperP2PBandwidthBroker } = require('../index.js')
|
||||||
|
|
||||||
|
test('hyper-p2p-bandwidth-broker: basic operation', async (t) => {
|
||||||
|
const m = new HyperP2PBandwidthBroker()
|
||||||
|
m.grant('peer-a', 1000)
|
||||||
|
t.ok(m.consume('peer-a', 400))
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hyper-p2p-bandwidth-broker: validation', async (t) => {
|
||||||
|
const m = new HyperP2PBandwidthBroker()
|
||||||
|
try {
|
||||||
|
m.grant(null, -1)
|
||||||
|
t.fail('expected throw')
|
||||||
|
} catch (e) {
|
||||||
|
t.ok(e instanceof Error)
|
||||||
|
}
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hyper-p2p-bandwidth-broker: getStats', async (t) => {
|
||||||
|
const m = new HyperP2PBandwidthBroker()
|
||||||
|
const s = m.getStats()
|
||||||
|
t.ok(s)
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hyper-p2p-bandwidth-broker: close idempotent', async (t) => {
|
||||||
|
const m = new HyperP2PBandwidthBroker()
|
||||||
|
await m.close()
|
||||||
|
await m.close()
|
||||||
|
t.pass()
|
||||||
|
})
|
||||||
@@ -10,3 +10,6 @@
|
|||||||
## v0.2.1
|
## v0.2.1
|
||||||
|
|
||||||
- Production docs, input validation, third test, integration notes.
|
- Production docs, input validation, third test, integration notes.
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||||
|
|||||||
@@ -18,15 +18,34 @@ const mod = new HyperP2PBucketRateLimit(opts)
|
|||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
See [`index.js`](../index.js) for the full method list. Core operations implement **distributed token-bucket rate limiting with gossip sync**.
|
| Method | Returns | Notes |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| `close(...)` | See source | — |
|
||||||
|
| `configure(...)` | See source | — |
|
||||||
|
| `getBucket(...)` | See source | — |
|
||||||
|
| `ready(...)` | See source | — |
|
||||||
|
| `snapshot(...)` | See source | — |
|
||||||
|
| `sync(...)` | See source | — |
|
||||||
|
| `tryConsume(...)` | See source | — |
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
The instance extends `EventEmitter`. Common events: `closed`, plus module-specific events documented in source.
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `closed` | Module-specific |
|
||||||
|
| `configure` | Module-specific |
|
||||||
|
| `consume` | Module-specific |
|
||||||
|
| `reject` | Module-specific |
|
||||||
|
| `remote-configure` | Module-specific |
|
||||||
|
| `sync` | Module-specific |
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Call `getStats()` when implemented for counters (Wave 6 network modules always expose stats).
|
||||||
|
|
||||||
## P2P
|
## P2P
|
||||||
|
|
||||||
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux channel `bucket-rate-limit/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux `bucket-rate-limit/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
# Architecture: hyper-p2p-bucket-rate-limit
|
# Architecture: hyper-p2p-bucket-rate-limit
|
||||||
|
|
||||||
|
Token-bucket rate limiting per peer for Bare/Pear P2P overlays.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart TB
|
||||||
App[Application] --> Mod[HyperP2PBucketRateLimit]
|
App[Application] --> Mod[HyperP2PBucketRateLimit]
|
||||||
Mod --> P2P[Protomux bucket-rate-limit/v1]
|
Mod --> Mux[Protomux bucket-rate-limit/v1]
|
||||||
P2P --> Swarm[Hyperswarm]
|
Mux --> Swarm[Hyperswarm topic]
|
||||||
```
|
```
|
||||||
|
|
||||||
Local state lives in memory maps/arrays; gossip merges remote updates when `topic` is configured.
|
## 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.
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-bucket-rate-limit",
|
"name": "hyper-p2p-bucket-rate-limit",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hyper-p2p-bucket-rate-limit",
|
"name": "hyper-p2p-bucket-rate-limit",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.6.7",
|
"b4a": "^1.6.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-bucket-rate-limit",
|
"name": "hyper-p2p-bucket-rate-limit",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"description": "Token-bucket rate limiting per peer for Bare/Pear P2P.",
|
"description": "Token-bucket rate limiting per peer for Bare/Pear P2P.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
|
|||||||
@@ -30,3 +30,22 @@ test('hyper-p2p-bucket-rate-limit: close without leak', async (t) => {
|
|||||||
await m.close()
|
await m.close()
|
||||||
t.pass()
|
t.pass()
|
||||||
})
|
})
|
||||||
|
test('hyper-p2p-bucket-rate-limit: validation rejects invalid input', async (t) => {
|
||||||
|
const m = new HyperP2PBucketRateLimit()
|
||||||
|
try {
|
||||||
|
if (typeof m.addNeighbor === 'function') m.addNeighbor(null)
|
||||||
|
else if (typeof m.buildCircuit === 'function') m.buildCircuit([])
|
||||||
|
else if (typeof m.grant === 'function') m.grant(null, -1)
|
||||||
|
else if (typeof m.enqueue === 'function') m.enqueue('bad', null)
|
||||||
|
else if (typeof m.reportSample === 'function') m.reportSample(null, -1, -1)
|
||||||
|
else if (typeof m.fanout === 'function') m.fanout(null, 0)
|
||||||
|
else if (typeof m.probe === 'function') m.probe(null)
|
||||||
|
else if (typeof m.resolve === 'function') m.resolve(null)
|
||||||
|
else if (typeof m.acquire === 'function') m.acquire(null)
|
||||||
|
else throw new Error('no validation hook')
|
||||||
|
t.fail('expected throw')
|
||||||
|
} catch (err) {
|
||||||
|
t.ok(err instanceof Error)
|
||||||
|
}
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|||||||
@@ -19,3 +19,6 @@
|
|||||||
## v0.2.0
|
## v0.2.0
|
||||||
|
|
||||||
- Production-grade docs, validation, and expanded tests.
|
- Production-grade docs, validation, and expanded tests.
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||||
|
|||||||
@@ -18,15 +18,31 @@ const mod = new CapabilityManager(opts)
|
|||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
See [`index.js`](../index.js) for the full method list. Core operations implement **capability tokens with delegation**.
|
| Method | Returns | Notes |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| `delegate(...)` | See source | — |
|
||||||
|
| `getPublicKey(...)` | See source | — |
|
||||||
|
| `hasCapability(...)` | See source | — |
|
||||||
|
| `if(...)` | See source | — |
|
||||||
|
| `issue(...)` | See source | — |
|
||||||
|
| `revoke(...)` | See source | — |
|
||||||
|
| `verify(...)` | See source | — |
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
The instance extends `EventEmitter`. Common events: `closed`, plus module-specific events documented in source.
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `capability-delegated` | Module-specific |
|
||||||
|
| `capability-issued` | Module-specific |
|
||||||
|
| `capability-revoked` | Module-specific |
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Call `getStats()` when implemented for counters (Wave 6 network modules always expose stats).
|
||||||
|
|
||||||
## P2P
|
## P2P
|
||||||
|
|
||||||
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux channel `unknown/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux `unknown/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
# Architecture: hyper-p2p-capabilities
|
# Architecture: hyper-p2p-capabilities
|
||||||
|
|
||||||
|
Capability tokens with delegation for Bare/Pear P2P overlays.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart TB
|
||||||
App[Application] --> Mod[CapabilityManager]
|
App[Application] --> Mod[CapabilityManager]
|
||||||
Mod --> P2P[Protomux unknown/v1]
|
Mod --> Mux[Protomux unknown/v1]
|
||||||
P2P --> Swarm[Hyperswarm]
|
Mux --> Swarm[Hyperswarm topic]
|
||||||
```
|
```
|
||||||
|
|
||||||
Local state lives in memory maps/arrays; gossip merges remote updates when `topic` is configured.
|
## 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.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-capabilities",
|
"name": "hyper-p2p-capabilities",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hyper-p2p-capabilities",
|
"name": "hyper-p2p-capabilities",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.6.7",
|
"b4a": "^1.6.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-capabilities",
|
"name": "hyper-p2p-capabilities",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"description": "Novel capability-based access control and authorization primitive for P2P applications in Bare/Pear. Cryptographic capability tokens, delegation, revocation, and integration with Hyperswarm and Hyperbee.",
|
"description": "Novel capability-based access control and authorization primitive for P2P applications in Bare/Pear. Cryptographic capability tokens, delegation, revocation, and integration with Hyperswarm and Hyperbee.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -19,3 +19,6 @@
|
|||||||
## v0.2.0
|
## v0.2.0
|
||||||
|
|
||||||
- Production-grade docs, validation, and expanded tests.
|
- Production-grade docs, validation, and expanded tests.
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||||
|
|||||||
@@ -18,15 +18,44 @@ const mod = new HyperP2PCausalConsensus(opts)
|
|||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
See [`index.js`](../index.js) for the full method list. Core operations implement **causal consensus proposals and votes**.
|
| Method | Returns | Notes |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| `addPeer(...)` | See source | — |
|
||||||
|
| `close(...)` | See source | — |
|
||||||
|
| `getAllDecided(...)` | See source | — |
|
||||||
|
| `getDecidedOrder(...)` | See source | — |
|
||||||
|
| `getMetrics(...)` | See source | — |
|
||||||
|
| `integrateVectorClock(...)` | See source | — |
|
||||||
|
| `propose(...)` | See source | — |
|
||||||
|
| `receiveProposal(...)` | See source | — |
|
||||||
|
| `receiveVote(...)` | See source | — |
|
||||||
|
| `vote(...)` | See source | — |
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
The instance extends `EventEmitter`. Common events: `closed`, plus module-specific events documented in source.
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `closed` | Module-specific |
|
||||||
|
| `consensus` | Module-specific |
|
||||||
|
| `error` | Module-specific |
|
||||||
|
| `fork-detected` | Module-specific |
|
||||||
|
| `gossip` | Module-specific |
|
||||||
|
| `hyperswarm-gossip` | Module-specific |
|
||||||
|
| `invalid-signature` | Module-specific |
|
||||||
|
| `order-decided` | Module-specific |
|
||||||
|
| `proposal` | Module-specific |
|
||||||
|
| `proposal-expired` | Module-specific |
|
||||||
|
| `proposal-received` | Module-specific |
|
||||||
|
| `protomux-send` | Module-specific |
|
||||||
|
| `vote` | Module-specific |
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Call `getStats()` when implemented for counters (Wave 6 network modules always expose stats).
|
||||||
|
|
||||||
## P2P
|
## P2P
|
||||||
|
|
||||||
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux channel `unknown/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux `unknown/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
# Architecture: hyper-p2p-causal-consensus
|
# Architecture: hyper-p2p-causal-consensus
|
||||||
|
|
||||||
|
Causal consensus proposals and votes for Bare/Pear P2P overlays.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart TB
|
||||||
App[Application] --> Mod[HyperP2PCausalConsensus]
|
App[Application] --> Mod[HyperP2PCausalConsensus]
|
||||||
Mod --> P2P[Protomux unknown/v1]
|
Mod --> Mux[Protomux unknown/v1]
|
||||||
P2P --> Swarm[Hyperswarm]
|
Mux --> Swarm[Hyperswarm topic]
|
||||||
```
|
```
|
||||||
|
|
||||||
Local state lives in memory maps/arrays; gossip merges remote updates when `topic` is configured.
|
## 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.
|
||||||
|
|||||||
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-causal-consensus",
|
"name": "hyper-p2p-causal-consensus",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hyper-p2p-causal-consensus",
|
"name": "hyper-p2p-causal-consensus",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.6.7",
|
"b4a": "^1.6.7",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../hyper-p2p-vector-clock": {
|
"../hyper-p2p-vector-clock": {
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.6.7",
|
"b4a": "^1.6.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-causal-consensus",
|
"name": "hyper-p2p-causal-consensus",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"description": "A novel, production-grade Byzantine Fault Tolerant (BFT) causal consensus primitive for Bare/Pear P2P applications. Provides decentralized total ordering of events with vector-clock causality tracking, cryptographic Ed25519 signing for proposals and votes, quorum-based agreement (2f+1 for f faults), fork detection, view-change recovery, Hyperbee persistence for decided orders, Hyperswarm topic discovery, and Protomux streaming for consensus messages. Enables building reliable decentralized ledgers, ordered event logs, multi-writer CRDTs with BFT guarantees, and fault-tolerant P2P microservices. First reusable dedicated BFT causal consensus module in the Holepunch/Bare ecosystem — never-before-seen primitive combining causality, threshold quorums, and tamper-proof ordering.",
|
"description": "A novel, production-grade Byzantine Fault Tolerant (BFT) causal consensus primitive for Bare/Pear P2P applications. Provides decentralized total ordering of events with vector-clock causality tracking, cryptographic Ed25519 signing for proposals and votes, quorum-based agreement (2f+1 for f faults), fork detection, view-change recovery, Hyperbee persistence for decided orders, Hyperswarm topic discovery, and Protomux streaming for consensus messages. Enables building reliable decentralized ledgers, ordered event logs, multi-writer CRDTs with BFT guarantees, and fault-tolerant P2P microservices. First reusable dedicated BFT causal consensus module in the Holepunch/Bare ecosystem — never-before-seen primitive combining causality, threshold quorums, and tamper-proof ordering.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
|
|||||||
@@ -209,3 +209,22 @@ test('hyper-p2p-causal-consensus: close without leak', async (t) => {
|
|||||||
await m.close()
|
await m.close()
|
||||||
t.pass()
|
t.pass()
|
||||||
})
|
})
|
||||||
|
test('hyper-p2p-causal-consensus: validation rejects invalid input', async (t) => {
|
||||||
|
const m = new HyperP2PCausalConsensus()
|
||||||
|
try {
|
||||||
|
if (typeof m.addNeighbor === 'function') m.addNeighbor(null)
|
||||||
|
else if (typeof m.buildCircuit === 'function') m.buildCircuit([])
|
||||||
|
else if (typeof m.grant === 'function') m.grant(null, -1)
|
||||||
|
else if (typeof m.enqueue === 'function') m.enqueue('bad', null)
|
||||||
|
else if (typeof m.reportSample === 'function') m.reportSample(null, -1, -1)
|
||||||
|
else if (typeof m.fanout === 'function') m.fanout(null, 0)
|
||||||
|
else if (typeof m.probe === 'function') m.probe(null)
|
||||||
|
else if (typeof m.resolve === 'function') m.resolve(null)
|
||||||
|
else if (typeof m.acquire === 'function') m.acquire(null)
|
||||||
|
else throw new Error('no validation hook')
|
||||||
|
t.fail('expected throw')
|
||||||
|
} catch (err) {
|
||||||
|
t.ok(err instanceof Error)
|
||||||
|
}
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
- Initial release: Multi-hop overlay circuits with CREATE/EXTEND/RELAY cells.
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# hyper-p2p-circuit-loom
|
||||||
|
|
||||||
|
Multi-hop overlay circuits with CREATE/EXTEND/RELAY cells.
|
||||||
|
|
||||||
|
**Protocol:** `circuit-loom/v1`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# API: hyper-p2p-circuit-loom
|
||||||
|
|
||||||
|
**Protocol:** `circuit-loom/v1`
|
||||||
|
|
||||||
|
**Export:** `HyperP2PCircuitLoom`
|
||||||
|
|
||||||
|
## Constructor
|
||||||
|
|
||||||
|
```js
|
||||||
|
const mod = new HyperP2PCircuitLoom(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 |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| `buildCircuit(...)` | See source | — |
|
||||||
|
| `close(...)` | See source | — |
|
||||||
|
| `extend(...)` | See source | — |
|
||||||
|
| `getStats(...)` | See source | — |
|
||||||
|
| `ready(...)` | See source | — |
|
||||||
|
| `relay(...)` | See source | — |
|
||||||
|
| `rotateKeys(...)` | See source | — |
|
||||||
|
| `teardown(...)` | See source | — |
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `circuit-built` | Module-specific |
|
||||||
|
| `closed` | Module-specific |
|
||||||
|
| `destroyed` | Module-specific |
|
||||||
|
| `exit-deliver` | Module-specific |
|
||||||
|
| `extended` | Module-specific |
|
||||||
|
| `keys-rotated` | Module-specific |
|
||||||
|
| `relay` | Module-specific |
|
||||||
|
| `remote-relay` | 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 `circuit-loom/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Integration: [`../../real_tests/integration/circuit-loom-two-node.js`](../../real_tests/integration/circuit-loom-two-node.js)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Architecture: hyper-p2p-circuit-loom
|
||||||
|
|
||||||
|
Multi-hop overlay circuits for Bare/Pear P2P overlays.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
App[Application] --> Mod[HyperP2PCircuitLoom]
|
||||||
|
Mod --> Mux[Protomux circuit-loom/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 { HyperP2PCircuitLoom } = require('..')
|
||||||
|
|
||||||
|
async function main () {
|
||||||
|
const m = new HyperP2PCircuitLoom()
|
||||||
|
const id = m.buildCircuit(['a','b']); m.relay(id, 'x')
|
||||||
|
console.log('ok', m.getStats())
|
||||||
|
await m.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error)
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
require('bare-process/global')
|
||||||
|
const EventEmitter = require('bare-events')
|
||||||
|
const b4a = require('b4a')
|
||||||
|
const crypto = require('hypercore-crypto')
|
||||||
|
const { initModuleSwarm, gossipSend } = require('../_shared/p2p-bare.js')
|
||||||
|
const PROTOCOL = 'circuit-loom/v1'
|
||||||
|
|
||||||
|
class HyperP2PCircuitLoom extends EventEmitter {
|
||||||
|
constructor (opts = {}) {
|
||||||
|
super()
|
||||||
|
this.topic = opts.topic || null
|
||||||
|
this.keyPair = opts.keyPair || crypto.keyPair()
|
||||||
|
this.peerId = b4a.toString(this.keyPair.publicKey, 'hex')
|
||||||
|
this._circuits = new Map()
|
||||||
|
this._stats = { created: 0, relays: 0 }
|
||||||
|
this.swarm = null
|
||||||
|
this._peerMsgs = null
|
||||||
|
}
|
||||||
|
|
||||||
|
_deriveKey (hopPeer) {
|
||||||
|
return crypto.hash(b4a.from(this.peerId + hopPeer))
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCircuit (hopPeers) {
|
||||||
|
if (!Array.isArray(hopPeers) || hopPeers.length < 1) throw new Error('hopPeers required')
|
||||||
|
const circuitId = b4a.toString(crypto.hash(b4a.from(hopPeers.join(':') + Date.now())), 'hex').slice(0, 16)
|
||||||
|
const hops = hopPeers.map((p, i) => ({ peer: p, key: this._deriveKey(p), index: i }))
|
||||||
|
const circuit = { circuitId, hops, state: 'open', createdAt: Date.now() }
|
||||||
|
this._circuits.set(circuitId, circuit)
|
||||||
|
this._stats.created++
|
||||||
|
if (this._peerMsgs) gossipSend(this, { type: 'CREATE', circuit })
|
||||||
|
this.emit('circuit-built', circuit)
|
||||||
|
return circuitId
|
||||||
|
}
|
||||||
|
|
||||||
|
extend (circuitId, nextHop) {
|
||||||
|
const c = this._circuits.get(circuitId)
|
||||||
|
if (!c) throw new Error('unknown circuit')
|
||||||
|
if (!nextHop) throw new Error('nextHop required')
|
||||||
|
c.hops.push({ peer: nextHop, key: this._deriveKey(nextHop), index: c.hops.length })
|
||||||
|
if (this._peerMsgs) gossipSend(this, { type: 'EXTEND', circuitId, hop: nextHop })
|
||||||
|
this.emit('extended', { circuitId, hop: nextHop })
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
relay (circuitId, payload) {
|
||||||
|
const c = this._circuits.get(circuitId)
|
||||||
|
if (!c || c.state !== 'open') return false
|
||||||
|
if (payload == null) throw new Error('payload required')
|
||||||
|
const buf = b4a.isBuffer(payload) ? payload : b4a.from(JSON.stringify(payload))
|
||||||
|
let cell = buf
|
||||||
|
for (const hop of c.hops) cell = crypto.hash(b4a.concat([cell, hop.key]))
|
||||||
|
const frame = { type: 'RELAY', circuitId, cell: b4a.toString(cell, 'hex'), from: this.peerId }
|
||||||
|
if (this._peerMsgs) gossipSend(this, frame)
|
||||||
|
this._stats.relays++
|
||||||
|
this.emit('relay', frame)
|
||||||
|
this.emit('exit-deliver', { circuitId, payload: buf, hops: c.hops.length })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown (circuitId) {
|
||||||
|
const c = this._circuits.get(circuitId)
|
||||||
|
if (!c) return false
|
||||||
|
c.state = 'closed'
|
||||||
|
this._circuits.delete(circuitId)
|
||||||
|
if (this._peerMsgs) gossipSend(this, { type: 'DESTROY', circuitId })
|
||||||
|
this.emit('destroyed', { circuitId })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateKeys (circuitId) {
|
||||||
|
const c = this._circuits.get(circuitId)
|
||||||
|
if (!c) throw new Error('unknown circuit')
|
||||||
|
for (const hop of c.hops) hop.key = this._deriveKey(hop.peer + ':rot' + Date.now())
|
||||||
|
this.emit('keys-rotated', { circuitId })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
getStats () { return { ...this._stats, open: [...this._circuits.values()].filter(c => c.state === 'open').length } }
|
||||||
|
|
||||||
|
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 === 'RELAY') this.emit('remote-relay', data)
|
||||||
|
else if (data?.type === 'CREATE' && data.circuit) this._circuits.set(data.circuit.circuitId, data.circuit)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
async close () {
|
||||||
|
if (this.swarm) await this.swarm.destroy().catch(() => {})
|
||||||
|
this.swarm = null
|
||||||
|
this._circuits.clear()
|
||||||
|
this.emit('closed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = { HyperP2PCircuitLoom, PROTOCOL }
|
||||||
Generated
+1790
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "hyper-p2p-circuit-loom",
|
||||||
|
"version": "0.3.0",
|
||||||
|
"description": "Multi-hop overlay circuits with CREATE/EXTEND/RELAY cells.",
|
||||||
|
"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,35 @@
|
|||||||
|
require('bare-process/global')
|
||||||
|
const test = require('brittle')
|
||||||
|
const { HyperP2PCircuitLoom } = require('../index.js')
|
||||||
|
|
||||||
|
test('hyper-p2p-circuit-loom: basic operation', async (t) => {
|
||||||
|
const m = new HyperP2PCircuitLoom()
|
||||||
|
const id = m.buildCircuit(['hop1', 'hop2'])
|
||||||
|
t.ok(m.relay(id, 'secret'))
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hyper-p2p-circuit-loom: validation', async (t) => {
|
||||||
|
const m = new HyperP2PCircuitLoom()
|
||||||
|
try {
|
||||||
|
m.buildCircuit([])
|
||||||
|
t.fail('expected throw')
|
||||||
|
} catch (e) {
|
||||||
|
t.ok(e instanceof Error)
|
||||||
|
}
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hyper-p2p-circuit-loom: getStats', async (t) => {
|
||||||
|
const m = new HyperP2PCircuitLoom()
|
||||||
|
const s = m.getStats()
|
||||||
|
t.ok(s)
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hyper-p2p-circuit-loom: close idempotent', async (t) => {
|
||||||
|
const m = new HyperP2PCircuitLoom()
|
||||||
|
await m.close()
|
||||||
|
await m.close()
|
||||||
|
t.pass()
|
||||||
|
})
|
||||||
@@ -7,3 +7,6 @@
|
|||||||
## v0.2.0
|
## v0.2.0
|
||||||
|
|
||||||
- Production-grade docs, validation, and expanded tests.
|
- Production-grade docs, validation, and expanded tests.
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||||
|
|||||||
@@ -18,15 +18,34 @@ const mod = new HyperP2PConflictSet(opts)
|
|||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
See [`index.js`](../index.js) for the full method list. Core operations implement **OR-Set CRDT**.
|
| Method | Returns | Notes |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| `add(...)` | See source | — |
|
||||||
|
| `close(...)` | See source | — |
|
||||||
|
| `has(...)` | See source | — |
|
||||||
|
| `merge(...)` | See source | — |
|
||||||
|
| `ready(...)` | See source | — |
|
||||||
|
| `remove(...)` | See source | — |
|
||||||
|
| `toJSON(...)` | See source | — |
|
||||||
|
| `values(...)` | See source | — |
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
The instance extends `EventEmitter`. Common events: `closed`, plus module-specific events documented in source.
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `add` | Module-specific |
|
||||||
|
| `closed` | Module-specific |
|
||||||
|
| `merge` | Module-specific |
|
||||||
|
| `remote-op` | Module-specific |
|
||||||
|
| `remove` | Module-specific |
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Call `getStats()` when implemented for counters (Wave 6 network modules always expose stats).
|
||||||
|
|
||||||
## P2P
|
## P2P
|
||||||
|
|
||||||
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux channel `conflict-set/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux `conflict-set/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
# Architecture: hyper-p2p-conflict-set
|
# Architecture: hyper-p2p-conflict-set
|
||||||
|
|
||||||
|
Or-set crdt for Bare/Pear P2P overlays.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart TB
|
||||||
App[Application] --> Mod[HyperP2PConflictSet]
|
App[Application] --> Mod[HyperP2PConflictSet]
|
||||||
Mod --> P2P[Protomux conflict-set/v1]
|
Mod --> Mux[Protomux conflict-set/v1]
|
||||||
P2P --> Swarm[Hyperswarm]
|
Mux --> Swarm[Hyperswarm topic]
|
||||||
```
|
```
|
||||||
|
|
||||||
Local state lives in memory maps/arrays; gossip merges remote updates when `topic` is configured.
|
## 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.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-conflict-set",
|
"name": "hyper-p2p-conflict-set",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hyper-p2p-conflict-set",
|
"name": "hyper-p2p-conflict-set",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.6.7",
|
"b4a": "^1.6.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-conflict-set",
|
"name": "hyper-p2p-conflict-set",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"description": "OR-Set CRDT conflict-free set for Bare/Pear P2P.",
|
"description": "OR-Set CRDT conflict-free set for Bare/Pear P2P.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
|
|||||||
@@ -25,3 +25,22 @@ test('hyper-p2p-conflict-set: close without leak', async (t) => {
|
|||||||
await m.close()
|
await m.close()
|
||||||
t.pass()
|
t.pass()
|
||||||
})
|
})
|
||||||
|
test('hyper-p2p-conflict-set: validation rejects invalid input', async (t) => {
|
||||||
|
const m = new HyperP2PConflictSet()
|
||||||
|
try {
|
||||||
|
if (typeof m.addNeighbor === 'function') m.addNeighbor(null)
|
||||||
|
else if (typeof m.buildCircuit === 'function') m.buildCircuit([])
|
||||||
|
else if (typeof m.grant === 'function') m.grant(null, -1)
|
||||||
|
else if (typeof m.enqueue === 'function') m.enqueue('bad', null)
|
||||||
|
else if (typeof m.reportSample === 'function') m.reportSample(null, -1, -1)
|
||||||
|
else if (typeof m.fanout === 'function') m.fanout(null, 0)
|
||||||
|
else if (typeof m.probe === 'function') m.probe(null)
|
||||||
|
else if (typeof m.resolve === 'function') m.resolve(null)
|
||||||
|
else if (typeof m.acquire === 'function') m.acquire(null)
|
||||||
|
else throw new Error('no validation hook')
|
||||||
|
t.fail('expected throw')
|
||||||
|
} catch (err) {
|
||||||
|
t.ok(err instanceof Error)
|
||||||
|
}
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
- Initial release: Gossip RTT/loss samples and recommended send rates.
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# hyper-p2p-congestion-signal
|
||||||
|
|
||||||
|
Gossip RTT/loss samples and recommended send rates.
|
||||||
|
|
||||||
|
**Protocol:** `congestion-signal/v1`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# API: hyper-p2p-congestion-signal
|
||||||
|
|
||||||
|
**Protocol:** `congestion-signal/v1`
|
||||||
|
|
||||||
|
**Export:** `HyperP2PCongestionSignal`
|
||||||
|
|
||||||
|
## Constructor
|
||||||
|
|
||||||
|
```js
|
||||||
|
const mod = new HyperP2PCongestionSignal(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 | — |
|
||||||
|
| `getHint(...)` | See source | — |
|
||||||
|
| `getStats(...)` | See source | — |
|
||||||
|
| `ready(...)` | See source | — |
|
||||||
|
| `reportSample(...)` | See source | — |
|
||||||
|
| `shouldThrottle(...)` | See source | — |
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `closed` | Module-specific |
|
||||||
|
| `sample` | 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 `congestion-signal/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Integration: [`../../real_tests/integration/congestion-signal-two-node.js`](../../real_tests/integration/congestion-signal-two-node.js)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Architecture: hyper-p2p-congestion-signal
|
||||||
|
|
||||||
|
Gossip congestion hints for Bare/Pear P2P overlays.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
App[Application] --> Mod[HyperP2PCongestionSignal]
|
||||||
|
Mod --> Mux[Protomux congestion-signal/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 { HyperP2PCongestionSignal } = require('..')
|
||||||
|
|
||||||
|
async function main () {
|
||||||
|
const m = new HyperP2PCongestionSignal()
|
||||||
|
m.reportSample('peer', 30, 0)
|
||||||
|
console.log('ok', m.getStats())
|
||||||
|
await m.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error)
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
require('bare-process/global')
|
||||||
|
const EventEmitter = require('bare-events')
|
||||||
|
const { initModuleSwarm, gossipSend } = require('../_shared/p2p-bare.js')
|
||||||
|
const PROTOCOL = 'congestion-signal/v1'
|
||||||
|
|
||||||
|
class HyperP2PCongestionSignal extends EventEmitter {
|
||||||
|
constructor (opts = {}) {
|
||||||
|
super()
|
||||||
|
this.topic = opts.topic || null
|
||||||
|
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
|
||||||
|
this.baseRateBps = opts.baseRateBps || 1_000_000
|
||||||
|
this._samples = new Map()
|
||||||
|
this._stats = { samples: 0 }
|
||||||
|
this.swarm = null
|
||||||
|
this._peerMsgs = null
|
||||||
|
}
|
||||||
|
|
||||||
|
reportSample (peerId, rttMs, loss = 0) {
|
||||||
|
if (!peerId) throw new Error('peerId required')
|
||||||
|
if (rttMs < 0 || loss < 0 || loss > 1) throw new Error('invalid rtt or loss')
|
||||||
|
const s = { peerId, rttMs, loss, at: Date.now() }
|
||||||
|
this._samples.set(peerId, s)
|
||||||
|
this._stats.samples++
|
||||||
|
if (this._peerMsgs) gossipSend(this, { type: 'congestion-sample', sample: s })
|
||||||
|
this.emit('sample', s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
getHint (peerId) {
|
||||||
|
const s = this._samples.get(peerId)
|
||||||
|
if (!s) return { sendRateBps: this.baseRateBps }
|
||||||
|
const factor = Math.max(0.1, 1 - s.loss - (s.rttMs / 1000) * 0.1)
|
||||||
|
return { sendRateBps: Math.floor(this.baseRateBps * factor), rttMs: s.rttMs, loss: s.loss }
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldThrottle (peerId) {
|
||||||
|
const h = this.getHint(peerId)
|
||||||
|
return h.sendRateBps < this.baseRateBps * 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
getStats () { return { ...this._stats, peers: this._samples.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 === 'congestion-sample' && data.sample) {
|
||||||
|
const cur = this._samples.get(data.sample.peerId)
|
||||||
|
if (!cur || data.sample.at >= cur.at) this._samples.set(data.sample.peerId, data.sample)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
async close () {
|
||||||
|
if (this.swarm) await this.swarm.destroy().catch(() => {})
|
||||||
|
this.swarm = null
|
||||||
|
this._samples.clear()
|
||||||
|
this.emit('closed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = { HyperP2PCongestionSignal, PROTOCOL }
|
||||||
+1790
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "hyper-p2p-congestion-signal",
|
||||||
|
"version": "0.3.0",
|
||||||
|
"description": "Gossip RTT/loss samples and recommended send rates.",
|
||||||
|
"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,35 @@
|
|||||||
|
require('bare-process/global')
|
||||||
|
const test = require('brittle')
|
||||||
|
const { HyperP2PCongestionSignal } = require('../index.js')
|
||||||
|
|
||||||
|
test('hyper-p2p-congestion-signal: basic operation', async (t) => {
|
||||||
|
const m = new HyperP2PCongestionSignal()
|
||||||
|
m.reportSample('p1', 50, 0.1)
|
||||||
|
t.ok(m.getHint('p1').sendRateBps > 0)
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hyper-p2p-congestion-signal: validation', async (t) => {
|
||||||
|
const m = new HyperP2PCongestionSignal()
|
||||||
|
try {
|
||||||
|
m.reportSample(null, -1, 0)
|
||||||
|
t.fail('expected throw')
|
||||||
|
} catch (e) {
|
||||||
|
t.ok(e instanceof Error)
|
||||||
|
}
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hyper-p2p-congestion-signal: getStats', async (t) => {
|
||||||
|
const m = new HyperP2PCongestionSignal()
|
||||||
|
const s = m.getStats()
|
||||||
|
t.ok(s)
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hyper-p2p-congestion-signal: close idempotent', async (t) => {
|
||||||
|
const m = new HyperP2PCongestionSignal()
|
||||||
|
await m.close()
|
||||||
|
await m.close()
|
||||||
|
t.pass()
|
||||||
|
})
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
- Initial release: Warm peer connection lanes with idle teardown.
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# hyper-p2p-connection-pool
|
||||||
|
|
||||||
|
Warm peer connection lanes with idle teardown.
|
||||||
|
|
||||||
|
**Protocol:** `connection-pool/v1`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# API: hyper-p2p-connection-pool
|
||||||
|
|
||||||
|
**Protocol:** `connection-pool/v1`
|
||||||
|
|
||||||
|
**Export:** `HyperP2PConnectionPool`
|
||||||
|
|
||||||
|
## Constructor
|
||||||
|
|
||||||
|
```js
|
||||||
|
const mod = new HyperP2PConnectionPool(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 |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| `acquire(...)` | See source | — |
|
||||||
|
| `close(...)` | See source | — |
|
||||||
|
| `getPoolStats(...)` | See source | — |
|
||||||
|
| `getStats(...)` | See source | — |
|
||||||
|
| `ready(...)` | See source | — |
|
||||||
|
| `release(...)` | See source | — |
|
||||||
|
| `setIdleTimeout(...)` | See source | — |
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `acquire` | Module-specific |
|
||||||
|
| `closed` | Module-specific |
|
||||||
|
| `idle-closed` | Module-specific |
|
||||||
|
| `release` | 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 `connection-pool/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Integration: [`../../real_tests/integration/connection-pool-two-node.js`](../../real_tests/integration/connection-pool-two-node.js)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Architecture: hyper-p2p-connection-pool
|
||||||
|
|
||||||
|
Peer connection pool lifecycle for Bare/Pear P2P overlays.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
App[Application] --> Mod[HyperP2PConnectionPool]
|
||||||
|
Mod --> Mux[Protomux connection-pool/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 { HyperP2PConnectionPool } = require('..')
|
||||||
|
|
||||||
|
async function main () {
|
||||||
|
const m = new HyperP2PConnectionPool()
|
||||||
|
m.acquire('peer'); m.release('peer')
|
||||||
|
console.log('ok', m.getStats())
|
||||||
|
await m.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
require('bare-process/global')
|
||||||
|
const EventEmitter = require('bare-events')
|
||||||
|
const timers = require('bare-timers')
|
||||||
|
const { initModuleSwarm, gossipSend } = require('../_shared/p2p-bare.js')
|
||||||
|
const PROTOCOL = 'connection-pool/v1'
|
||||||
|
|
||||||
|
class HyperP2PConnectionPool extends EventEmitter {
|
||||||
|
constructor (opts = {}) {
|
||||||
|
super()
|
||||||
|
this.topic = opts.topic || null
|
||||||
|
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
|
||||||
|
this.maxConcurrent = opts.maxConcurrent || 32
|
||||||
|
this.idleTimeoutMs = opts.idleTimeoutMs || 60000
|
||||||
|
this.enableBackgroundTimers = opts.enableBackgroundTimers === true
|
||||||
|
this._lanes = new Map()
|
||||||
|
this._stats = { acquires: 0, releases: 0, idleClosed: 0 }
|
||||||
|
this._sweep = null
|
||||||
|
this.swarm = null
|
||||||
|
this._peerMsgs = null
|
||||||
|
}
|
||||||
|
|
||||||
|
setIdleTimeout (ms) {
|
||||||
|
if (ms < 0) throw new Error('idle timeout must be non-negative')
|
||||||
|
this.idleTimeoutMs = ms
|
||||||
|
}
|
||||||
|
|
||||||
|
acquire (peerId) {
|
||||||
|
if (!peerId) throw new Error('peerId required')
|
||||||
|
if (this._lanes.size >= this.maxConcurrent && !this._lanes.has(peerId)) {
|
||||||
|
throw new Error('maxConcurrent exceeded')
|
||||||
|
}
|
||||||
|
const lane = this._lanes.get(peerId) || { peerId, state: 'open', uses: 0, lastUsed: 0 }
|
||||||
|
lane.state = 'open'
|
||||||
|
lane.uses++
|
||||||
|
lane.lastUsed = Date.now()
|
||||||
|
this._lanes.set(peerId, lane)
|
||||||
|
this._stats.acquires++
|
||||||
|
this.emit('acquire', lane)
|
||||||
|
return lane
|
||||||
|
}
|
||||||
|
|
||||||
|
release (peerId) {
|
||||||
|
if (!peerId) throw new Error('peerId required')
|
||||||
|
const lane = this._lanes.get(peerId)
|
||||||
|
if (!lane) return false
|
||||||
|
lane.state = 'idle'
|
||||||
|
lane.lastUsed = Date.now()
|
||||||
|
this._stats.releases++
|
||||||
|
this.emit('release', lane)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
_sweepIdle () {
|
||||||
|
const now = Date.now()
|
||||||
|
for (const [id, lane] of this._lanes) {
|
||||||
|
if (lane.state === 'idle' && now - lane.lastUsed > this.idleTimeoutMs) {
|
||||||
|
this._lanes.delete(id)
|
||||||
|
this._stats.idleClosed++
|
||||||
|
this.emit('idle-closed', { peerId: id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPoolStats () {
|
||||||
|
return {
|
||||||
|
open: [...this._lanes.values()].filter(l => l.state === 'open').length,
|
||||||
|
idle: [...this._lanes.values()].filter(l => l.state === 'idle').length,
|
||||||
|
total: this._lanes.size,
|
||||||
|
...this._stats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStats () { return this.getPoolStats() }
|
||||||
|
|
||||||
|
async ready () {
|
||||||
|
if (this.swarm || !this.topic) return this
|
||||||
|
await initModuleSwarm(this, {
|
||||||
|
keyPair: this.keyPair, topic: this.topic, protocol: PROTOCOL,
|
||||||
|
onmessage: () => {}
|
||||||
|
})
|
||||||
|
if (this.enableBackgroundTimers) {
|
||||||
|
this._sweep = timers.setInterval(() => this._sweepIdle(), Math.min(this.idleTimeoutMs, 5000))
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
async close () {
|
||||||
|
if (this._sweep) clearInterval(this._sweep)
|
||||||
|
this._sweep = null
|
||||||
|
if (this.swarm) await this.swarm.destroy().catch(() => {})
|
||||||
|
this.swarm = null
|
||||||
|
this._lanes.clear()
|
||||||
|
this.emit('closed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = { HyperP2PConnectionPool, PROTOCOL }
|
||||||
+1790
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "hyper-p2p-connection-pool",
|
||||||
|
"version": "0.3.0",
|
||||||
|
"description": "Warm peer connection lanes with idle teardown.",
|
||||||
|
"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,36 @@
|
|||||||
|
require('bare-process/global')
|
||||||
|
const test = require('brittle')
|
||||||
|
const { HyperP2PConnectionPool } = require('../index.js')
|
||||||
|
|
||||||
|
test('hyper-p2p-connection-pool: basic operation', async (t) => {
|
||||||
|
const m = new HyperP2PConnectionPool()
|
||||||
|
m.acquire('peer-x')
|
||||||
|
m.release('peer-x')
|
||||||
|
t.is(m.getPoolStats().total, 1)
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hyper-p2p-connection-pool: validation', async (t) => {
|
||||||
|
const m = new HyperP2PConnectionPool()
|
||||||
|
try {
|
||||||
|
m.acquire(null)
|
||||||
|
t.fail('expected throw')
|
||||||
|
} catch (e) {
|
||||||
|
t.ok(e instanceof Error)
|
||||||
|
}
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hyper-p2p-connection-pool: getStats', async (t) => {
|
||||||
|
const m = new HyperP2PConnectionPool()
|
||||||
|
const s = m.getStats()
|
||||||
|
t.ok(s)
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hyper-p2p-connection-pool: close idempotent', async (t) => {
|
||||||
|
const m = new HyperP2PConnectionPool()
|
||||||
|
await m.close()
|
||||||
|
await m.close()
|
||||||
|
t.pass()
|
||||||
|
})
|
||||||
@@ -6,3 +6,6 @@
|
|||||||
## v0.2.0
|
## v0.2.0
|
||||||
|
|
||||||
- Production-grade docs, validation, and expanded tests.
|
- Production-grade docs, validation, and expanded tests.
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||||
|
|||||||
@@ -18,15 +18,30 @@ const mod = new HyperP2PContradictionGraph(opts)
|
|||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
See [`index.js`](../index.js) for the full method list. Core operations implement **opposing claims contradiction lattice**.
|
| Method | Returns | Notes |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| `claim(...)` | See source | — |
|
||||||
|
| `close(...)` | See source | — |
|
||||||
|
| `conflictSet(...)` | See source | — |
|
||||||
|
| `contradicts(...)` | See source | — |
|
||||||
|
| `merge(...)` | See source | — |
|
||||||
|
| `ready(...)` | See source | — |
|
||||||
|
| `toJSON(...)` | See source | — |
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
The instance extends `EventEmitter`. Common events: `closed`, plus module-specific events documented in source.
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `claim` | Module-specific |
|
||||||
|
| `closed` | Module-specific |
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Call `getStats()` when implemented for counters (Wave 6 network modules always expose stats).
|
||||||
|
|
||||||
## P2P
|
## P2P
|
||||||
|
|
||||||
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux channel `contradiction-graph/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux `contradiction-graph/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
# Architecture: hyper-p2p-contradiction-graph
|
# Architecture: hyper-p2p-contradiction-graph
|
||||||
|
|
||||||
|
Opposing claims contradiction lattice for Bare/Pear P2P overlays.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart TB
|
||||||
App[Application] --> Mod[HyperP2PContradictionGraph]
|
App[Application] --> Mod[HyperP2PContradictionGraph]
|
||||||
Mod --> P2P[Protomux contradiction-graph/v1]
|
Mod --> Mux[Protomux contradiction-graph/v1]
|
||||||
P2P --> Swarm[Hyperswarm]
|
Mux --> Swarm[Hyperswarm topic]
|
||||||
```
|
```
|
||||||
|
|
||||||
Local state lives in memory maps/arrays; gossip merges remote updates when `topic` is configured.
|
## 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.
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-contradiction-graph",
|
"name": "hyper-p2p-contradiction-graph",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hyper-p2p-contradiction-graph",
|
"name": "hyper-p2p-contradiction-graph",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.6.7",
|
"b4a": "^1.6.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-contradiction-graph",
|
"name": "hyper-p2p-contradiction-graph",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"description": "Opposing claims contradiction lattice for Bare/Pear P2P.",
|
"description": "Opposing claims contradiction lattice for Bare/Pear P2P.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
|
|||||||
@@ -25,3 +25,22 @@ test('hyper-p2p-contradiction-graph: close without leak', async (t) => {
|
|||||||
await m.close()
|
await m.close()
|
||||||
t.pass()
|
t.pass()
|
||||||
})
|
})
|
||||||
|
test('hyper-p2p-contradiction-graph: validation rejects invalid input', async (t) => {
|
||||||
|
const m = new HyperP2PContradictionGraph()
|
||||||
|
try {
|
||||||
|
if (typeof m.addNeighbor === 'function') m.addNeighbor(null)
|
||||||
|
else if (typeof m.buildCircuit === 'function') m.buildCircuit([])
|
||||||
|
else if (typeof m.grant === 'function') m.grant(null, -1)
|
||||||
|
else if (typeof m.enqueue === 'function') m.enqueue('bad', null)
|
||||||
|
else if (typeof m.reportSample === 'function') m.reportSample(null, -1, -1)
|
||||||
|
else if (typeof m.fanout === 'function') m.fanout(null, 0)
|
||||||
|
else if (typeof m.probe === 'function') m.probe(null)
|
||||||
|
else if (typeof m.resolve === 'function') m.resolve(null)
|
||||||
|
else if (typeof m.acquire === 'function') m.acquire(null)
|
||||||
|
else throw new Error('no validation hook')
|
||||||
|
t.fail('expected throw')
|
||||||
|
} catch (err) {
|
||||||
|
t.ok(err instanceof Error)
|
||||||
|
}
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|||||||
@@ -6,3 +6,6 @@
|
|||||||
## v0.2.0
|
## v0.2.0
|
||||||
|
|
||||||
- Production-grade docs, validation, and expanded tests.
|
- Production-grade docs, validation, and expanded tests.
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- Wave 6: presence-tier API tables, architecture wire section, validation test.
|
||||||
|
|||||||
@@ -18,15 +18,32 @@ const mod = new HyperP2PCrdtMap(opts)
|
|||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
See [`index.js`](../index.js) for the full method list. Core operations implement **LWW-Map CRDT**.
|
| Method | Returns | Notes |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| `close(...)` | See source | — |
|
||||||
|
| `delete(...)` | See source | — |
|
||||||
|
| `get(...)` | See source | — |
|
||||||
|
| `keys(...)` | See source | — |
|
||||||
|
| `merge(...)` | See source | — |
|
||||||
|
| `ready(...)` | See source | — |
|
||||||
|
| `set(...)` | See source | — |
|
||||||
|
| `toJSON(...)` | See source | — |
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
The instance extends `EventEmitter`. Common events: `closed`, plus module-specific events documented in source.
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `closed` | Module-specific |
|
||||||
|
| `merge` | Module-specific |
|
||||||
|
| `set` | Module-specific |
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Call `getStats()` when implemented for counters (Wave 6 network modules always expose stats).
|
||||||
|
|
||||||
## P2P
|
## P2P
|
||||||
|
|
||||||
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux channel `crdt-map/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
When `topic` is set, `ready()` joins Hyperswarm and opens Protomux `crdt-map/v1` via [`_shared/p2p-bare.js`](../_shared/p2p-bare.js).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
# Architecture: hyper-p2p-crdt-map
|
# Architecture: hyper-p2p-crdt-map
|
||||||
|
|
||||||
|
Lww-map crdt for Bare/Pear P2P overlays.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart TB
|
||||||
App[Application] --> Mod[HyperP2PCrdtMap]
|
App[Application] --> Mod[HyperP2PCrdtMap]
|
||||||
Mod --> P2P[Protomux crdt-map/v1]
|
Mod --> Mux[Protomux crdt-map/v1]
|
||||||
P2P --> Swarm[Hyperswarm]
|
Mux --> Swarm[Hyperswarm topic]
|
||||||
```
|
```
|
||||||
|
|
||||||
Local state lives in memory maps/arrays; gossip merges remote updates when `topic` is configured.
|
## 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.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hyper-p2p-crdt-map",
|
"name": "hyper-p2p-crdt-map",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hyper-p2p-crdt-map",
|
"name": "hyper-p2p-crdt-map",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.6.7",
|
"b4a": "^1.6.7",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user