Updates
This commit is contained in:
@@ -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 }
|
||||
+1774
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()
|
||||
})
|
||||
Reference in New Issue
Block a user