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-memetic-spread
Production module: Meme spread tracker.
**Protocol:** `memetic-spread/v1`
## When to use
Idea spread models.
## When not to use
Real analytics.
## Quick start
```js
const { HyperP2PMemeticSpread } = require('hyper-p2p-memetic-spread')
const m = new HyperP2PMemeticSpread()
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,82 @@
# API: hyper-p2p-memetic-spread
**Protocol:** `memetic-spread/v1`
**Export:** `{ HyperP2PMemeticSpread, PROTOCOL }`
## Overview
`HyperP2PMemeticSpread` tracks meme infection across peer ids: each `infect(memeId, peer)` adds a host and sets `strain` to host count (local) or max(remote strain, hosts).
## Constructor
```js
const { HyperP2PMemeticSpread } = require('hyper-p2p-memetic-spread')
const spread = new HyperP2PMemeticSpread({ topic: 'meme-mesh' })
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `topic` | string | `null` | Hyperswarm topic |
| `keyPair` | KeyPair | random | Swarm identity |
## Methods
### `infect(memeId, peer) → { memeId, peer, strain }`
- **Throws:** `assertNonEmpty` on `memeId`, `peer`
- Gossips `meme-infect`; emits `infect`
### `strain(memeId) → number`
Host-set size for meme, or `0`.
### `spreadStats() → object`
`{ totalMemes, totalHosts, byMeme: { [id]: { strain, hosts } } }`
### Lifecycle
`ready()`, `close()` (emits `closed`), `getStats()`.
## Meme record (internal)
`{ memeId, hosts: Set, strain: number, firstSeen }`
## P2P wire
| type | fields |
|------|--------|
| `meme-infect` | `memeId`, `peer`, `strain`, `at` |
## Events
`infect`, `closed`
## Strain semantics
`strain` is recomputed as `hosts.size` after each local `infect`. Remote gossip uses `Math.max(local.strain, d.strain || hosts.size)` so the metric reflects reach, not viral velocity. `firstSeen` is set on first local observation only.
## Selector patterns
Use stable `memeId` strings (content hash, campaign id). Use `peer` as hex pubkey or logical agent id consistent across your mesh.
## Limitations
No decay or cure — hosts accumulate until process restart. For production epidemiology models, add TTL in your app layer.
## Gossip payload
`meme-infect` includes `at: Date.now()` from publisher; receivers do not rewrite timestamp.
## Stats
`getStats()``{ infections, gossipIn, gossipOut, memes, protocol }`.
## Testing
```bash
npm install && npm test
```
Example: [`../examples/basic.js`](../examples/basic.js).
@@ -0,0 +1,17 @@
# Architecture: hyper-p2p-memetic-spread
**Category:** `experimental` · **Protocol:** `memetic-spread/v1`
## Wire messages
| type | direction | fields | behavior |
|------|-----------|--------|----------|
| `meme-infect` | gossip | `memeId`, `peer`, `strain`, `at` | Union hosts; `strain = max(local, remote)` |
## State model
`_memes: Map(memeId → { hosts Set, strain, firstSeen })`
## Composition
Experimental analytics alongside `hyper-p2p-gossip-mesh` for fanout.
@@ -0,0 +1,8 @@
require('bare-process/global')
const { HyperP2PMemeticSpread } = require('../index.js')
async function main () {
const m = new HyperP2PMemeticSpread()
console.log(m.getStats())
await m.close()
}
main().catch(console.error)
@@ -0,0 +1,100 @@
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 = 'memetic-spread/v1'
class HyperP2PMemeticSpread extends EventEmitter {
constructor (opts = {}) {
super()
this.topic = opts.topic || null
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
this._memes = new Map()
this._stats = { infections: 0, gossipIn: 0, gossipOut: 0 }
this.swarm = null
this._peerMsgs = null
}
infect (memeId, peer) {
assertNonEmpty(memeId, 'memeId')
assertNonEmpty(peer, 'peer')
const meme = this._memes.get(memeId) || {
memeId,
hosts: new Set(),
strain: 1,
firstSeen: Date.now()
}
meme.hosts.add(peer)
meme.strain = meme.hosts.size
this._memes.set(memeId, meme)
this._stats.infections++
this._gossip({
type: 'meme-infect',
memeId,
peer,
strain: meme.strain,
at: Date.now()
})
this.emit('infect', { memeId, peer, strain: meme.strain })
return { memeId, peer, strain: meme.strain }
}
strain (memeId) {
assertNonEmpty(memeId, 'memeId')
const meme = this._memes.get(memeId)
return meme ? meme.strain : 0
}
spreadStats () {
const stats = { totalMemes: this._memes.size, totalHosts: 0, byMeme: {} }
for (const [id, meme] of this._memes) {
stats.totalHosts += meme.hosts.size
stats.byMeme[id] = { strain: meme.strain, hosts: meme.hosts.size }
}
return stats
}
_gossip (data) {
if (!this._peerMsgs) return
gossipSend(this, data)
this._stats.gossipOut++
}
_onGossip (d) {
if (!d || d.type !== 'meme-infect' || !d.memeId) return
this._stats.gossipIn++
const meme = this._memes.get(d.memeId) || {
memeId: d.memeId,
hosts: new Set(),
strain: 1,
firstSeen: Date.now()
}
meme.hosts.add(d.peer)
meme.strain = Math.max(meme.strain, d.strain || meme.hosts.size)
this._memes.set(d.memeId, meme)
}
getStats () {
return { ...this._stats, memes: this._memes.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 () {
if (this.swarm) await this.swarm.destroy().catch(() => {})
this.swarm = null
this.emit('closed')
}
}
module.exports = { HyperP2PMemeticSpread, PROTOCOL }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
{
"name": "hyper-p2p-memetic-spread",
"version": "0.3.1",
"description": "Memetic spread model.",
"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,36 @@
require('bare-process/global')
const test = require('brittle')
const { HyperP2PMemeticSpread, PROTOCOL } = require('../index.js')
test('exports', (t) => {
t.ok(HyperP2PMemeticSpread)
t.is(PROTOCOL, 'memetic-spread/v1')
})
test('infect increases strain', async (t) => {
const m = new HyperP2PMemeticSpread()
m.infect('meme-1', 'peer-a')
m.infect('meme-1', 'peer-b')
t.is(m.strain('meme-1'), 2)
await m.close()
})
test('spreadStats', async (t) => {
const m = new HyperP2PMemeticSpread()
m.infect('m', 'p')
t.is(m.spreadStats().totalMemes, 1)
await m.close()
})
test('validation', async (t) => {
const m = new HyperP2PMemeticSpread()
try { m.infect(null, 'p') } catch (e) { t.ok(e) }
await m.close()
})
test('getStats', async (t) => {
const m = new HyperP2PMemeticSpread()
m.infect('x', 'y')
t.is(m.getStats().infections, 1)
await m.close()
})