This commit is contained in:
Raven Scott
2026-05-20 22:56:32 -04:00
parent b8c335adee
commit dd384eb944
321 changed files with 7484 additions and 3793 deletions
@@ -1,28 +1,37 @@
# hyper-p2p-crdt-version-vector
**Status:** scaffold (`0.0.0-scaffold`) · **Protocol:** `crdt-version-vector/v1` · **Wave:** 8
Per-peer logical clocks for causality and conflict detection.
Version vector CRDT.
**Protocol:** `crdt-version-vector/v1`
## Holepunch references (inspiration only)
## When to use
- `hyper-p2p-vector-clock`
- Detecting concurrent edits before merging application state.
- Tracking causality across replicated peers.
> This module composes on Hyperswarm/Hypercore — it does **not** re-implement upstream packages.
## When not to use
## Composes with
- Storing application payload (use LWW register or OR-map).
- Global numeric counters (use PN-counter).
- `hyper-p2p-crdt-map`
## Quick start
## Planned API
```js
const { HyperP2PCrdtVersionVector } = require('hyper-p2p-crdt-version-vector')
const vv = new HyperP2PCrdtVersionVector({ topic: 'vv-demo' })
await vv.ready()
vv.increment('peer-a')
const order = vv.compare(vv.toJSON(), { 'peer-b': 2 })
await vv.close()
```
- `constructor(opts)` — topic, optional keyPair
- `getStats()` — scaffold counters
- `ready()` — no-op until implemented
- Domain methods — throw `not implemented: scaffold` until Wave 8+ pass
## Docs
## Layout
- [docs/api.md](docs/api.md)
- [docs/architecture.md](docs/architecture.md)
`modules/state-crdts/hyper-p2p-crdt-version-vector/`
## Test
See [`modules/_shared/MODULE_SYSTEM.md`](../../_shared/MODULE_SYSTEM.md).
```bash
npm install && npm test
```
@@ -1,23 +1,21 @@
# hyper-p2p-crdt-version-vector API
# API: hyper-p2p-crdt-version-vector
**Status:** scaffold · **Protocol:** `crdt-version-vector/v1`
**Protocol:** `crdt-version-vector/v1` · **Export:** `HyperP2PCrdtVersionVector`, `compareVectors`
## Class `HyperP2PCrdtVersionVector`
## Methods
Scaffold stub — methods throw `not implemented: scaffold` until promoted to production tier.
### `increment(peer)`
### `constructor(opts?)`
Increments this peer's counter and gossips.
### `getStats()`
### `merge(other)`
Returns `{ created, errors, protocol, tier: 'scaffold' }`.
Pointwise max merge: `other` is `{ peerId: count }`.
### `ready()`
### `compare(a, b)`
Resolves immediately (no-op).
Returns `'equal' | 'before' | 'after' | 'concurrent'`.
## Wire (planned)
### `toJSON()` / `getStats()` / `ready()` / `close()`
| Message | Direction | Notes |
|---------|-----------|-------|
| TBD | gossip | Defined in implementation pass |
Standard lifecycle; gossip message `crdt-version-vector-sync` with `{ peer, value }`.
@@ -1,15 +1,13 @@
# hyper-p2p-crdt-version-vector architecture
# Architecture: hyper-p2p-crdt-version-vector
**Tier:** scaffold · **Category:** `state-crdts`
## Model
## Role
Map of peer → monotonic integer. `increment(peer)` only advances the local peer entry on this replica; remote peers advance via gossip merge (max).
Version vector CRDT.
## Compare
## Composition
`compareVectors` implements standard dominates relation: `after` if all components ≥ and at least one >; `before` if dominated; `concurrent` if incomparable; `equal` if identical.
Uses `../../_shared/p2p-bare.js` for Hyperswarm + Protomux when implemented. Does **not** duplicate Holepunch core storage/transport.
## Gossip
## Holepunch boundary
Inspiration: n/a
`initModuleSwarm` when `topic` set; inbound sync applies max per peer without re-broadcast.
@@ -1,8 +1,12 @@
require('bare-process/global')
const { HyperP2PCrdtVersionVector } = require('../index.js')
const { HyperP2PCrdtVersionVector, compareVectors } = require('../index.js')
async function main () {
const m = new HyperP2PCrdtVersionVector()
console.log('[scaffold]', m.getStats())
const vv = new HyperP2PCrdtVersionVector()
vv.increment('local')
vv.merge({ remote: 3 })
console.log(vv.toJSON(), compareVectors(vv.toJSON(), { local: 1 }), vv.getStats())
await vv.close()
}
main().catch(console.error)
@@ -1,54 +1,94 @@
require('bare-process/global')
const EventEmitter = require('bare-events')
const { assertNonEmpty } = require('../../_shared/lib/errors.js')
const { attachGossip, sendGossip } = require('../../_shared/storage-gossip-base.js')
const { initModuleSwarm, gossipSend } = require('../../_shared/p2p-bare.js')
const PROTOCOL = 'crdt-version-vector/v1'
function compareVectors (a, b) {
const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})])
let aDom = false
let bDom = false
for (const k of keys) {
const av = a[k] || 0
const bv = b[k] || 0
if (av > bv) aDom = true
if (bv > av) bDom = true
}
if (!aDom && !bDom) return 'equal'
if (aDom && !bDom) return 'after'
if (bDom && !aDom) return 'before'
return 'concurrent'
}
class HyperP2PCrdtVersionVector extends EventEmitter {
constructor (opts = {}) {
super()
this.topic = opts.topic || null
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
this._data = new Map()
this._clock = new Map()
this._stats = { ops: 0, gossipIn: 0, gossipOut: 0 }
this.swarm = null
this._peerMsgs = null
}
set (key, value) {
assertNonEmpty(key, 'key')
this._data.set(key, value)
increment (peer) {
assertNonEmpty(peer, 'peer')
const v = (this._clock.get(peer) || 0) + 1
this._clock.set(peer, v)
this._stats.ops++
sendGossip(this, { type: 'crdt-version-vector-sync', key, value })
return true
gossipSend(this, { type: 'crdt-version-vector-sync', peer, value: v })
this._stats.gossipOut++
return v
}
get (key) { return this._data.get(key) }
merge (remote) {
for (const [k, v] of Object.entries(remote || {})) this._data.set(k, v)
merge (other) {
if (!other || typeof other !== 'object') return 0
let n = 0
for (const [peer, value] of Object.entries(other)) {
const cur = this._clock.get(peer) || 0
if (value > cur) {
this._clock.set(peer, value)
n++
}
}
return n
}
toJSON () { return Object.fromEntries(this._data) }
compare (a, b) {
return compareVectors(a, b)
}
toJSON () {
return Object.fromEntries(this._clock)
}
_onGossip (d) {
if (!d || d.type !== 'crdt-version-vector-sync') return
if (!d || d.type !== 'crdt-version-vector-sync' || !d.peer) return
this._stats.gossipIn++
if (d.key) this.set(d.key, d.value)
this.merge({ [d.peer]: d.value })
}
getStats () { return { ...this._stats, keys: this._data.size, protocol: PROTOCOL } }
getStats () {
return { ...this._stats, peers: this._clock.size, protocol: PROTOCOL }
}
async ready () {
if (this.swarm || !this.topic) return this
await attachGossip(this, { keyPair: this.keyPair, topic: this.topic, protocol: PROTOCOL, onmessage: (d) => this._onGossip(d) })
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._peerMsgs = null
}
}
module.exports = { HyperP2PCrdtVersionVector, PROTOCOL }
module.exports = { HyperP2PCrdtVersionVector, PROTOCOL, compareVectors }
@@ -1,26 +1,36 @@
require('bare-process/global')
const test = require('brittle')
const { HyperP2PCrdtVersionVector, PROTOCOL } = require('../index.js')
const { HyperP2PCrdtVersionVector, PROTOCOL, compareVectors } = require('../index.js')
test('exports', (t) => {
t.ok(HyperP2PCrdtVersionVector)
t.ok(PROTOCOL)
t.is(PROTOCOL, 'crdt-version-vector/v1')
})
test('basic operation', async (t) => {
test('increment and merge', async (t) => {
const m = new HyperP2PCrdtVersionVector()
m.set('k', 1); t.ok(m.toJSON())
m.increment('p1')
m.merge({ p2: 3 })
t.is(m.toJSON().p1, 1)
t.is(m.toJSON().p2, 3)
await m.close()
})
test('compare vectors', (t) => {
t.is(compareVectors({ a: 2 }, { a: 1 }), 'after')
t.is(compareVectors({ a: 1 }, { a: 2 }), 'before')
t.is(compareVectors({ a: 1, b: 2 }, { a: 2, b: 1 }), 'concurrent')
})
test('validation', async (t) => {
const m = new HyperP2PCrdtVersionVector()
try { m.put(null, 1) } catch (e) { t.ok(e) }
try { m.increment(null); t.fail('expected throw') } catch (e) { t.ok(e) }
await m.close()
})
test('getStats', async (t) => {
const m = new HyperP2PCrdtVersionVector()
t.ok(m.getStats().protocol)
m.increment('x')
t.is(m.getStats().protocol, PROTOCOL)
await m.close()
})