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