This commit is contained in:
Raven Scott
2026-05-20 23:57:33 -04:00
parent ac60c93151
commit 226fca591d
86 changed files with 200 additions and 162 deletions
@@ -0,0 +1,5 @@
# Changelog
## [0.0.0-scaffold] — Wave 8
- Registry scaffold: file tree, load smoke tests, docs stubs
@@ -0,0 +1,34 @@
# hyper-p2p-void-channel
Production module: Void channels.
**Protocol:** `void-channel/v1`
## When to use
Absence signaling.
## When not to use
Regular pub/sub.
## Quick start
```js
const { HyperP2PVoidChannel } = require('hyper-p2p-void-channel')
const m = new HyperP2PVoidChannel()
await m.ready()
await m.close()
```
## Docs
- [docs/api.md](docs/api.md)
- [docs/architecture.md](docs/architecture.md)
- [examples/basic.js](examples/basic.js)
## Test
```bash
npm install && npm test
```
@@ -0,0 +1,87 @@
# API: hyper-p2p-void-channel
**Protocol:** `void-channel/v1` · **Export:** `HyperP2PVoidChannel`, `PROTOCOL`
## Overview
Experimental void-message channel: publishes void events per named `channel` with metadata, delivers to subscribers and gossips `void-publish` to peers.
## Constructor
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `topic` | `string` \| `Buffer` | `null` | Swarm topic |
| `keyPair` | `KeyPair` | random | Identity |
## Methods
### `publishVoid(channel, meta?)`
- **Returns:** `{ channel, meta, at, voided: true }`
- **Throws:** `assertNonEmpty` on `channel`
- **Gossip:** `{ type: 'void-publish', channel, meta, at }`
- **Side effect:** `_deliver` to subscriber and `void` event
### `subscribeVoid(channel, fn)`
- **Returns:** unsubscribe function
- **Throws:** `fn must be a function`
- Replays last void for channel if present
### `ready()` / `close()`
`close` clears subscribers.
## Events
| Event | Payload |
|-------|---------|
| `void` | void message object |
| `closed` | — |
## getStats()
`published`, `received`, `subscriptions`, `gossipIn`, `gossipOut`, `channels` (subs size), `voids` (cache size), `protocol`.
## Wire
| type | fields | behavior |
|------|--------|----------|
| `void-publish` | `channel`, `meta`, `at` | Store and `_deliver` |
## Errors
`assertNonEmpty`, `fn must be a function`.
See [`../../_shared/ERROR_CODES.md`](../../_shared/ERROR_CODES.md).
## P2P
Void messages are idempotent per channel (latest stored in `_voids`).
## Testing
```bash
cd modules/experimental/hyper-p2p-void-channel && npm test
```
## Composition
Experimental wave5 modules; pair with `hyper-p2p-topic-announcer` for channel discovery.
## Example
See [`examples/basic.js`](../examples/basic.js).
## Remote merge rules
- `void-publish` always updates `_voids` and calls `_deliver`
- Subscriber `fn` errors are not caught on deliver (caller should guard)
## Lifecycle
Unsubscribe via function returned from `subscribeVoid`.
## See also
[`docs/architecture.md`](architecture.md), [`../../MODULE_CATEGORIES.md`](../../MODULE_CATEGORIES.md).
@@ -0,0 +1,18 @@
# Architecture: hyper-p2p-void-channel
**Protocol:** `void-channel/v1` · **Category:** experimental
## Wire messages
| type | direction | fields | behavior |
|------|-----------|--------|----------|
| `void-publish` | gossip | `channel`, `meta`, `at` | Cache in `_voids`; deliver to subscriber |
## State model
- `_subs`: Map channel → callback
- `_voids`: Map channel → last message
## Composition
`hyper-p2p-topic-announcer`, experimental wave5 peers.
@@ -0,0 +1,8 @@
require('bare-process/global')
const { HyperP2PVoidChannel } = require('../index.js')
async function main () {
const m = new HyperP2PVoidChannel()
console.log(m.getStats())
await m.close()
}
main().catch(console.error)
@@ -0,0 +1,92 @@
require('bare-process/global')
const EventEmitter = require('bare-events')
const { assertNonEmpty } = require('../../_shared/lib/errors.js')
const { initModuleSwarm, gossipSend } = require('../../_shared/p2p-bare.js')
const PROTOCOL = 'void-channel/v1'
class HyperP2PVoidChannel extends EventEmitter {
constructor (opts = {}) {
super()
this.topic = opts.topic || null
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
this._subs = new Map()
this._voids = new Map()
this._stats = { published: 0, received: 0, subscriptions: 0, gossipIn: 0, gossipOut: 0 }
this.swarm = null
this._peerMsgs = null
}
publishVoid (channel, meta) {
assertNonEmpty(channel, 'channel')
const msg = { channel, meta: meta || {}, at: Date.now(), voided: true }
this._voids.set(channel, msg)
this._stats.published++
this._deliver(channel, msg)
this._gossip({ type: 'void-publish', channel, meta: msg.meta, at: msg.at })
return msg
}
subscribeVoid (channel, fn) {
assertNonEmpty(channel, 'channel')
if (typeof fn !== 'function') throw new Error('fn must be a function')
this._subs.set(channel, fn)
this._stats.subscriptions++
if (this._voids.has(channel)) {
fn(this._voids.get(channel))
}
return () => this._subs.delete(channel)
}
_deliver (channel, msg) {
const fn = this._subs.get(channel)
if (fn) {
this._stats.received++
fn(msg)
}
this.emit('void', msg)
}
_gossip (data) {
if (!this._peerMsgs) return
gossipSend(this, data)
this._stats.gossipOut++
}
_onGossip (d) {
if (!d || d.type !== 'void-publish' || !d.channel) return
this._stats.gossipIn++
const msg = { channel: d.channel, meta: d.meta || {}, at: d.at || Date.now(), voided: true }
this._voids.set(d.channel, msg)
this._deliver(d.channel, msg)
}
getStats () {
return {
...this._stats,
channels: this._subs.size,
voids: this._voids.size,
protocol: PROTOCOL
}
}
async ready () {
if (this.swarm || !this.topic) return this
await initModuleSwarm(this, {
keyPair: this.keyPair,
topic: this.topic,
protocol: PROTOCOL,
onmessage: (d) => this._onGossip(d)
})
return this
}
async close () {
this._subs.clear()
if (this.swarm) await this.swarm.destroy().catch(() => {})
this.swarm = null
this.emit('closed')
}
}
module.exports = { HyperP2PVoidChannel, PROTOCOL }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
{
"name": "hyper-p2p-void-channel",
"version": "0.3.1",
"description": "Void channel suppression.",
"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",
"b4a": "^1.6.7",
"hypercore-crypto": "^3.0.0",
"protomux": "^3.0.0",
"compact-encoding": "^2.16.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" }
}
}
@@ -0,0 +1,40 @@
require('bare-process/global')
const test = require('brittle')
const { HyperP2PVoidChannel, PROTOCOL } = require('../index.js')
test('exports', (t) => {
t.ok(HyperP2PVoidChannel)
t.is(PROTOCOL, 'void-channel/v1')
})
test('publishVoid delivers to subscriber', async (t) => {
const m = new HyperP2PVoidChannel()
let got = null
m.subscribeVoid('empty', (msg) => { got = msg })
m.publishVoid('empty', { reason: 'gone' })
t.is(got.meta.reason, 'gone')
t.ok(got.voided)
await m.close()
})
test('late subscribe gets retained void', async (t) => {
const m = new HyperP2PVoidChannel()
m.publishVoid('ch', { n: 1 })
let got = null
m.subscribeVoid('ch', (msg) => { got = msg })
t.is(got.meta.n, 1)
await m.close()
})
test('validation', async (t) => {
const m = new HyperP2PVoidChannel()
try { m.subscribeVoid('x', 'not-fn') } catch (e) { t.ok(e) }
await m.close()
})
test('getStats', async (t) => {
const m = new HyperP2PVoidChannel()
m.publishVoid('a', {})
t.is(m.getStats().published, 1)
await m.close()
})